Sitecore Azure Search: Top 10 Tips

Its been a while since I first wrote about Azure Search and we have a few more tips and tricks on how to optimise Azure Search implementations.

Before proceeding if you missed our previous posts check out some tools we created for Azure Search Helix setup and Geo-Spatial Searching.

Also, check out the slides from our presentation at last years Melbourne Sitecore User Group.

Ok let us jump into the top 10 tips:

Tip 1) Create custom indexes for targeted searching

The default out of the box indexes will attempt to cover just about everything in your Sitecore databases. They do so to support Sitecore CMS UI searches out of the box.  It’s not a problem if you want to use the default indexes (web, master) to search with, however for optimal searches and faster re-indexing time a custom index will help performance.

By stepping back and looking at the different search requirements across the site you can map out your custom indexes and the data that each will require.

Consider also that if the custom indexes need to be used across multiple Feature Helix modules the configuration files and search repositories may need to live in an appropriate Foundation module. More about feature vs foundation can be found here.

Tip 2) Keep your indexes lean

This tip follows on from the first Tip.

Essentially the default Azure Search configuration out of the box will have:

<indexAllFields>true</indexAllFields>

This can include a lot of fields and your probably not going to need every single Sitecore field in order to present the user with meaningful data on the front end interfaces.

The other option is to specify only the fields that you need in your indexes:

<include hint="list:IncludeField"> 
<Text>{A60ACD61-A6DB-4182-8329-C957982CEC74}</Text> 
</include>

The end result will limit the amount of JSON payload that needs to be sent across the network and also the amount of payload that the Sitecore Azure Search Provider needs to process.

Particularly if you are returning thousands of search results you can see what happens when “IndexAllFields” is on via Fiddler.

This screenshot is via a local development machine and Azure Search instance at the Microsoft hosting centre.

Fiddler Index

JSONFIelds

  • So for a single query “IndexAllFields” can result in:
    • 2 MB plus JSON payload size.
    • Document results with all Sitecore metadata included. That could be around 100 fields.

If your query results in Document counts in the thousands obviously the payload will grow rapidly. By reducing the fields in your indexes (removing un-necessary data)  you can speed up query, transfer and processing times and get the data displayed quicker.

Tip 3) Make use of direct azure connections

Sitecore has done a lot of the heavy lifting for you in the Sitecore Azure Search Provider. It’s a bit like a wrapper that does all the hard work for you. In some cases however you may find that writing your own queries that connect via the Azure Search DLL gives you better performance.

Tip 4) Monitor performance via Azure Search Portal

It’s really important to monitor your Azure Search Instance via Azure Portal. This will give you critical clues as to whether your scaling settings are appropriate.

In particular look out for high latency times as this will indicate that your search queries are getting throttled. As a result, you may need to scale up your Azure Search Instance.

In order to monitor your latency times go to:

  1. Login to Azure Portal
  2. Navigate to your Azure Search Instance.
  3. Click on metrics in the left-hand navigation
    • metrics
  4. Select the “Search Latency” checkbox and scan over the last week.
    • graph
  5. You will see some peaks these usually indicate heavy periods of re-indexing. During re-indexing, the Azure Search instance is under heavy load. As long as your peaks under 0.5-second mark your ok.  If you see Search Latency up into the 2-second timeframe you probably need to either adjust how your indexes are used (caching and re-indexing) or scale up to avoid the flow on effects of slow search.

Tip 5) Cache Wrappers

In the code that uses Azure Search, it would be advisable to use cache wrappers around the searches when possible. For your most common searches, this should prevent Azure Search getting hit repeatedly with the same query.

For a full example of cache wrapper checkout the section titled Sitecore.Caching.CustomCache in my previous blog post.

Tip 6) Disable Indexing on CD

This is a hot tip that we got from Sitecore Support when we started to encounter high search latency during re-indexing.

Most likely in your production setup, you will have a single Azure Search instance shared between CM and CD environments.

You need to factor in that CM should be the server that controls the re-indexing (writing) and CD will most likely be the server doing the queries (reading).

Re-indexing is triggered via the event queue and every server subscribes and reacts to these events. Each server with the out of the box search configuration will cause the Azure Search indexes to be updated.  In a shared Azure Search (or SOLR instance) this only needs to be updated by a single server. Each additional re-index is overkill and just doubling up on re-indexing workload.

You can, therefore, adjust the configuration on the CD servers so that it does not cause re-indexing to happen.

The trick is in your index configuration files to use Configuration Roles to specify the indexing strategy on each server.

 <strategies hint="list:AddStrategy">
 <!--
 NOTE: order of these is controls the execution order 
 -->
 <strategy role:require="Standalone OR ContentManagement" ref="contentSearch/indexConfigurations/indexUpdateStrategies/onPublishEndAsync"/>
 <strategy role:require="ContentDelivery" ref="contentSearch/indexConfigurations/indexUpdateStrategies/manual"/>
 </strategies>

Setting the index update strategy to manual on your CD servers will take a big load off your remote indexes.

Particularly if you have multiple CD servers using the same indexes. Each additional CD server would cause additional updates to the index without the above setting.

Tip 7) Rigid Indexes – Have a deployment plan

If your deployment includes additions and changes to the indexes and you need 100% availability of search data, a deployment plan for re-indexing will be required.

Grant chatted about the problem in his post here. To get around this you could consider using the blue / green paradigm during deployments.

  • This would mean having a set blue indexes and a set of green indexes.
  • Using slot swaps for your deployments.
    • One slot points to green in configuration.
    • One slot (production) points to blue in configuration.
  • To save on costs you could decommission the staging slot between deployments.

Tip 8) HttpClient should be a singleton or static

The basic idea here is that you should keep the number of HttpClient instances in your code to an absolute minimum if you want optimal performance.

The Sitecore Azure Search provider actually spins up 2 x HttpClient connections for every single index. This in itself is not ideal and unfortunately, there is not a lot you can do about this code in the core product itself.

In your own connections to other APIs, however, HttpClient SendAsync is perfectly thread safe.

By using HttpClient singletons you stand to gain big in the performance stakes. One great blog article worth reading runs you through the performance benefits. 

It’s also worth noting that in the Azure Search documentation Microsoft themselves say you should treat HttpClient as a singleton.

Tip 9) Monitor your resources

In Azure web apps you have finite resources with your app server plans. Opening multiple connections with HttpClient and not disposing of them properly can have severe consequences.

For instance, we found a bug in the core Sitecore product that was caused by the connection retryer. It held open ports forever whenever we hit out Azure Search plan usage limits.  The result was that we hit outbound open connection limits for sockets and this caused our Sitecore instance to ground to a slow halt.

Sitecore has since resolved the issue mentioned above after a lengthy investigation working alongside the Aceik team. This was tracked under reference number 203909.

To monitor the number of sockets in Azure we found a nice page on the MSDN site.

Tip 10) Make use of OData Expressions

This tip relates strongly to tip 3.  Azure search has some really powerful OData Expressions that you can make use of by a direct connection.  Once you have had a play with direct connections it is surprisingly easy to spin up really fast queries.

Operators include:

  • OrderBy, Filter (by field), Search
  • Logical operators (and, or, not).
  • Comparison expressions (eq, ne, gt, lt, ge, le).
  • any with no parameters. This tests whether a field of type Collection(Edm.String) contains any elements.
  • any and all with limited lambda expression support.
  • Geospatial functions geo.distance and geo.intersects. The geo.distance function returns the distance in kilometres between two points.

See the complete list here.


 

Q&A

Q) Anything on multiple region setups? Or latency considerations?

A) Multi-region setups:   Although I can’t comment from experience the configuration documentation does state that you can specify multiple Azure Search instances using a pipe separator in the connection string.

<add name="cloud.search" connectionString="serviceUrl=https://searchservice1.search.windows.net;apiVersion=2015-02-28;apiKey=AdminKey1|serviceUrl=https://searchservice2.search.windows.net;apiVersion=2015-02-28;apiKey=AdminKey2" /> 

Unfortunately, the documentation does not go into much detail. It simply states that “Sitecore supports a Search service with geo-replicated scenarios” which one would hope means under the hood it has all the smarts to take care of this.

I’m curious about this as well and opened a stack overflow ticket. Let’s see if anyone else in the community can answer this for us.

Search Latency: 

Search latency can be directly improved by adding more replicas via the scaling setting in Azure Portal

replicas

Two replicas should be your starting point for an Azure Search instance to support Sitecore. Once you launch your site you will need to follow the instruction in tip 4 above monitor search latency.  If the latency graph is showing consistent spikes and high latency times above 0.5 seconds it’s probably time to add some more replicas.

Aceik’s Jason Horne Wins Sitecore “Most Valuable Professional” Award 2018

Sitecore_MVP_logo_Technology_2018.jpgElite distinction awarded for exceptional contributions to the Sitecore ecosystem

Melbourne, Victoria, Australia — January 31, 2018 — Aceik – Sitecore Specialists, today announced that Jason Horne, Director and Chief Architect has been named a “Most Valuable Professional (MVP)” in Technology by Sitecore®, the global leader in experience management software. Jason Horne was one of only 208 Technology MVPs worldwide to be named a Sitecore MVP this year.

Now in its 12th year, Sitecore’s MVP program recognizes individual technology, strategy, and commerce advocates who share their Sitecore passion and expertise to offer positive customer experiences that drive business results. The Sitecore MVP Award recognizes the most active Sitecore experts from around the world who participate in online and offline communities to share their knowledge with other Sitecore partners and customers.

Aceik has a very close and strong relationship with Sitecore. We are very honored to be given this award in recognition of our expertise and contributions to Sitecore. Aceik is highly active in the Sitecore community providing thought leadership, presentations, workshops, open source modules, blog posts, co-founding and managing the Sitecore Melbourne user group and more. Through our deep engagement in the Sitecore community, Aceik stays on the cutting edge allowing us to provide the best advice and outcomes for our clients.

Aceik was founded in 2013 by Jason Horne as a Sitecore specialist consultancy (About Us, Services). Aceik is a one of a kind Australian Sitecore partner. We focus on Sitecore, .Net, Azure, and Agile, keeping this dedicated and deliberate focus allows us to stay on top of a very fast-paced industry. We strive to provide excellence in every Sitecore project we undertake.

Some things that make us unique Include:

  • 100% of our work is Sitecore related.
  • We guarantee Sitecore experts on every project.
  • Speak directly with our technical team.
  • We care about all our clients, they are our biggest advocates
  • We are responsive and enthusiastic about all projects big or small
  • We deliver quality solutions at the highest level, honestly and transparently

“The Sitecore MVP awards recognize and honor those individuals who make substantial contributions to our loyal community of partners and customers,” said Pieter Brinkman, Sitecore Senior Director of Technical Marketing. “MVPs consistently set a standard of excellence by delivering technical chops, enthusiasm, and a commitment to giving back to the Sitecore community. They truly understand and deliver on the power of the Sitecore Experience Platform to create personalized brand experiences for their consumers, driving revenue and customer loyalty.”

The Sitecore Experience Platform™ combines web content management, omnichannel digital delivery, insights into customer activity and engagement, and strategic digital marketing tools into a single, unified platform. Sitecore Experience Commerce™ 9, released in January 2018, is the only cloud-enabled platform that natively integrates content and commerce so brands can fully personalize and individualize the end-to-end shopping experience before, during, and after the transaction. Both platforms capture in real time every minute interaction—and intention—that customers and prospects have with a brand across digital and offline channels. The result is that Sitecore customers are able to use the platform to engage with prospects and customers in a highly personalized manner, earning long-term customer loyalty.

 Aceiks mission is to deliver quality solutions honestly and transparently. Through our deep engagement in the Sitecore community and singular focus we provide a substantial return on investment to our clients with every engagement. We believe that clients and vendors can work together as a single team to deliver outstanding results as true partners in a common goal.

Please feel free to get in touch:
Jason Horne, Founder, and Sitecore specialist
Aceik
jasonhorne@aceik.com.au
0426971867

More information can be found about the MVP Program on the Sitecore MVP site: http://www.sitecore.com/mvp

Downgrading Helix modules from Sitecore 8.2 to 7.2

Recently I had to update a Sitecore 7.2 site to use Helix architecture and bring some foundation modules in from a Sitecore 8.2 site.  There were several challenges with this process.  Here’s a few highlights:

Dependency Injection using Castle Windsor

For this site, Castle Windsor was being used for dependency injection.  In the Sitecore 8.2 site, dependencies are registered using a configurator in a .config patch.  This is not supported in Sitecore 7.2, so instead we have to do this in the Application_Start event in global.asax.cs.

In order to be able to use the same DI container throughout the code, I created a singleton container object (ContainerManager.Container):

public static class ContainerManager
 {
   private static IWindsorContainer _container;
   public static IWindsorContainer Container
   {
     get
     {
        if (_container != null) return _container;
        _container = new WindsorContainer();
        _container.Install(FromAssembly.This());
        return _container;
     }
   }
}

Then in global.asax.cs, I used that container to register the dependencies:

protected void Application_Start(object sender, EventArgs e)
 {
    _container = ContainerManager.Container;
    _container.Install(new RegisterGlassDependencies());
    ...

An example of the installer class for registering dependencies is shown below:

 public class RegisterGlassDependencies : IWindsorInstaller
 {
   public void Install(IWindsorContainer container, IConfigurationStore store)
   {
     container.Register(Component.For<ISitecoreContext>().ImplementedBy<SitecoreContext>());
     container.Register(Component.For<IGlassHtml>().ImplementedBy<GlassHtmlTemp>());
     container.Register(Component.For<IGlassFactory>().ImplementedBy<GlassFactory>());
   }
 }

In order for injected constructor parameters to resolve for Controllers, it is necessary to use a custom controller factory which uses the DI container to resolve the controller. There’s a fair bit of overriding going on, so here it comes.

Code for the controller factory shown below:

public class WindsorControllerFactory : DefaultControllerFactory
   {
       private readonly IWindsorContainer _container;
 
       public WindsorControllerFactory(IWindsorContainer container)
       {
           _container = container;
       }
 
       public override void ReleaseController(IController controller)
       {
           _container.Release(controller);
       }
 
       public override IController CreateController(RequestContext requestContext,string controllerName)
       {
           Assert.ArgumentNotNull(requestContext, "requestContext");
           Assert.ArgumentNotNull(controllerName, "controllerName");
           Type controllerType = null;
 
           if (TypeHelper.LooksLikeTypeName(controllerName))
           {
               controllerType = TypeHelper.GetType(controllerName);
           }
 
           if (controllerType == null)
           {
               controllerType = GetControllerType(
                   requestContext,
                   controllerName);
           }
 
           if (controllerType != null)
           {
               return (IController)_container.Resolve(controllerType);
           }
 
           return base.CreateController(requestContext, controllerName);
       }
   }

A pipeline was added with an “instead” patch for Sitecore.Mvc.Pipelines.Loader.InitializeControllerFactory.  This pipleine scans all assemblies and gets all classes based on IController then registers them with the Containermanager.Container.  Code shown below:

public class InitializeWindsorControllerFactory
   {
       public virtual void Process(ScapiPipelineArgs args)
       {
           SetupControllerFactory(args);
       }
 
       protected virtual void SetupControllerFactory(ScapiPipelineArgs args)
       {
           var container = ContainerManager.Container;
 
           //TODO: don't use hard-coded filter string
           var assemblies = GetAssemblies.GetByFilter("MyAssembly.*").Where(n => !n.FullName.StartsWith("MyAssembly.Service"))
               .Where(a => GetTypes.GetTypesImplementing<IController>(a).Any(x => x.Namespace != null && x.Namespace.StartsWith("MyNamespace")));
 
           foreach (var assembly in assemblies)
           {
               container.Register(Classes.FromAssembly(assembly).BasedOn<IController>().LifestyleTransient());
           }
 
           var controllerFactory = new WindsorControllerFactory(container);
 
           var scapiSitecoreControllerFactory = new
               ScapiSitecoreControllerFactory(controllerFactory);
 
           ControllerBuilder.Current.SetControllerFactory(scapiSitecoreControllerFactory);
       }
   }

Another pipeline was added with an “instead” patch for Sitecore.Mvc.Pipelines.Response.GetRenderer.GetControllerRenderer.  This pipeline allows us to use a custom controller renderer, as shown below:

public class GetControllerRenderer : Sitecore.Mvc.Pipelines.Response.GetRenderer.GetControllerRenderer
   {
       protected override Renderer GetRenderer(Rendering rendering, Sitecore.Mvc.Pipelines.Response.GetRenderer.GetRendererArgs args)
       {
           var renderer = base.GetRenderer(rendering, args);
           return !(renderer is ControllerRenderer) ? renderer : new CustomControllerRenderer(renderer as ControllerRenderer);
       }
   }

The custom controller renderer then allows us to use a custom controller runner as shown below:

public sealed class CustomControllerRenderer : ControllerRenderer
   {
       public CustomControllerRenderer(ControllerRenderer renderer)
       {
           ControllerName = renderer.ControllerName;
           ActionName = renderer.ActionName;
       }
 
       public override void Render(System.IO.TextWriter writer)
       {
           var controllerName = ControllerName;
           var actionName = ActionName;
           if (controllerName.IsWhiteSpaceOrNull() || actionName.IsWhiteSpaceOrNull())
           {
               return;
           }
           var controllerRunner = new CustomControllerRunner(controllerName, actionName);
           var value = controllerRunner.Execute();
           if (value.IsEmptyOrNull())
           {
               return;
           }
           writer.Write(value);
       }
 
   }

The custom controller runner then creates the controller using our custom controller factory with our DI container as a parameter.  This is our goal. Custom controller runner shown below:

public class CustomControllerRunner : ControllerRunner
   {
       public CustomControllerRunner(string controllerName, string actionName)
           : base(controllerName, actionName)
       { }
 
       protected override IController CreateController()
       {
           return CreateControllerUsingFactory();
       }
 
       private IController CreateControllerUsingFactory()
       {
           NeedRelease = true;
 
           var controllerFactory = new WindsorControllerFactory(ContainerManager.Container);
           return controllerFactory.CreateController(PageContext.Current.RequestContext, ControllerName);
       }
   }

There’s a few layers to it, but we got there in the end.

XUnit Tests

The tests that I moved over from the Sitecore 8.2 site used XUnit.  After moving them over, one of the errors I got in the Sitecore 7.2 site was:

Could not resolve type name: Sitecore.Data.DefaultDatabase, Sitecore.Kernel

To fix this,  for Sitecore versions prior to 8.2 all instances of ‘Sitecore.Data.DefaultDatabase, Sitecore.Kernel’ in the config files should be changed to ‘Sitecore.Data.Database, Sitecore.Kernel’.

One bit I got stuck on for a while was that there was a config file with this string in it that had not yet been added to the solution, but was there on the file system, and that was enough for this error to still be thrown.  It took a file system search for the string to track it down.

 

 

JS & CSS Minification an Alternative Helix Approach

On a recent project, we were looking at a way to meet a number of criteria to improve page load times.

With regard to CSS and JS requests these criteria were:

  • Reduce the number of CSS and JS HTTP requests on page load.
  • Reduce the size of CSS and JS files (minification)

At the same time, we were running a Sitecore Helix project and we needed this to seamlessly fit into our project and CI builds.

In the past, I had also done some Umbraco development and was familiar with a nice little package called “ClientDependency Framework (CDF) by Shannon Deminick”  that comes out of the box with that particular CMS.

CDF will cut down your server requests in a few ways:

  • Combining  (It combines multiple JS files into a single server call on the fly)
  • compressing
  • minifying output
  • Composition
  • Caching (Processing of files into a composite file it cached)
  • Persisting the combined composite files for increased performance when applications restart or when the Cache expires

It also has a development mode were asset includes are not touched so that you can debug as usual.

The problem we had to solve was how to make the above package work in a way that it could plug and play with the Helix way of doing things.

The habitat (helix example project)  provides a way to include assets file via a global theme and also on an idividual rendering level.

Global themes should be used to load assets that are to be applied across a whole website.

Rendering level assets should be used to load css and javascript that only apply directly to that particular rendering.

The current habitat solution contains a module called “Assets” that lives in the foundation layer. This module will cleverly round up all the assets that need to be rendered on the page by the Helix Architecture.

In order to use ClientDependency Framework (CDF) and seamlessly plug it into the Assets module, we have provided two new layout tags for use:

Usage in Layouts
- @CompositeAssetFileService.Current.RenderStyles()
- @CompositeAssetFileService.Current.RenderScript(ScriptLocation.Body)

The above integration is currently on a pull request (waiting approval) on the main Helix Habitat GitHub  repository:

See: https://github.com/Sitecore/Habitat/pull/349

It will remain available on Aceik’s fork of the habitat project. 

Setup Steps: 

To use this service you will need to rename the following and run the gulp build:
/// – “App_Config/ClientDependency.config.disabled”. to App_Config/ClientDependency.config
/// – “\src\Foundation\Assets\code\Web.config.transform.ClientDependency.minification.example” to Web.config.transform

Once running (not in debug mode) you will see the following asset includes in the html source.

/DependencyHandler.axd/bdc200f5bb6df7e817066f4b98499322/12/js

A note about Cloudflare and minification: 

If your using cloudflare in front of your website,  JS and CSS minification can be turned on as feature in Cloudflare.  You can also compact the number of server requests made by using a feature called Rocket Loader. 

Understanding Form Submission Tracking (WFFM and xDB)

This blog is targeted at a marketing audience that may be wondering how to interpret the WFFM tracking metrics, as shown in the form reports.

Definitions of metrics as proposed by Sitecore:

  • Visits – the total number of visitors who visited the page containing the web form.
  • Submission attempts – the total number of times that visitors clicked the submit button.
  • Dropouts – the total number of times that visitors filled in form fields but did not submit the form.
  • Successful submissions – the total number of times that visitors successfully submitted the web form and data was collected.

FormTracking

If you’re trying to test the above values by doing form submissions yourself and you don’t fully understand what is happening in the background, it will get confusing very quickly. Indeed some clients have asked me to investigate the tallies above as they believed the report was broken when some of the numbers started declining.

You really need to understand that just closing your browser when trying to affect the above metrics doesn’t fool the xDB into thinking your a different user.

Thanks to xDB cookies the values shown next to the metrics above can actually be reassigned.  What I mean by this is if a user is recorded as a dropout and they then return a few hours later to re-submit the form. The tally next to dropouts will actually reduce by one and that value will be added to successful submissions.  If you’re trying to test the tally for correctness a drop in some of these counts will leave you scratching your head.

The only guaranteed way for the user to be treated and a unique visitor is to clear your cookies and browser temp data.

A good way to perform these tests is to use “Incognito mode in chrome“. As this will prevent cookies from being stored.


This provides us with an explanation as to why you might see some of the tallies go into the negative. Which makes sense when you realise that dropouts can be converted to successful submissions.  How then do we explain when submission attempts drop in number?

Running a few tests on this reveals that when a user is converted from a drop-out to a successful submission, the submission attempts recorded against this user are also adjusted down.


The main takeaway from this is that xDB is clever and knows when the same user returns to a form.  It adjusts the tallies in the form report accordingly.

If your marketing department doesn’t care about how well a user is tracked and these tallies confuse them. Let’s say it wants to track the exact number of times a form was submitted (same user or not). You could achieve this independently just by tracking the number of times the thank-you page is loaded.

 

Advanced Scheduled Publishing

Why do we want to schedule publishing?

  1. We want to schedule publishing to ensure that content scheduled to go live will go live when expected. By default, if a scheduled publish isn’t setup any content scheduled to go live will have to wait until the next manual publish after the scheduled date/time.
  2. We want to remove the publish option from editors so that publishing isn’t over utilised.
  3. Reducing the amount of publishes in a day reduces the number of times the Sitecore HTML cache is cleared and therefore increase site performance.

The out of the box scheduled publishing agent provided by Sitecore solves point 1 and 2 above but the issue is publishing at times when a publish is not required. The default publishing agent will publish every x interval all day. This is not efficient because it is publishing when possibly no changes have occurred and that will still cause the HTML cache to clear.

Our Advanced Scheduled Publishing Module provides the option to schedule a publish between a start and finish date and set the interval. For example 9-5 and every 60 minutes.

It also provides the option to set up one-off scheduled publishes at a specific time. So, for example, one publish at 2pm and one at 2am.

These two options can be used individually or in combination. In combination you can allow for a common scenario where we want to publish every 60mins from 9am – 6pm and then a one-off publish at 12am so that any content scheduled to be published will do so ready for the new day.

The configuration of these intervals are managed via a configuration item you create and manage in Sitecore. The documentation found here. The last publish time is updated here also, allowing editors to have more visibility on when the last publish occurred.

Our module is built on top of the Sitecore scheduled tasks so it will check for a time and interval within the range of the scheduled task frequency so, therefore, could occur slightly earlier or later depending on your frequency value. This is a well-documented limitation as the Sitecore scheduled tasks run within the context of a web application which could go down, or be taken down at any time.

Links

Sitecore Marketplace

Github repo

 

Sitecore Helix: Lets Talk Layers

Here are some notes from the decision-making process our team uses with regard to what goes where and in which layer. Of course, the helix documentation does go over the guidelines but it’s not until you start working with the architecture that things begin to become clear.

Project Layer

Definition: The Project layer provides the context of the solution. This means the actual cohesive website or channel output from the implementation, such as the page types, layout and graphical design. It is on this layer that all the features of the solution are stitched together into a cohesive solution that fits the requirements.

Comment: The project layer is probably the most straightforward layer to understand. In our project, the modules in this layer remained lightweight and mostly contain razor view files that allow content editors to build up the HTML structure of the pages.

The website content (under home), page templates and component templates are serialized by Unicorn and also live in this layer.

It’s also worth mentioning one particular gotcha you may hit in development to do with template inheritance and you can read more in this blog post.

Feature or Foundation?

Feature Definition: The Feature layer contains concrete features of the solution as understood by the business owners and editors of the solution, for example news, articles, promotions, website search, etc. The features are expressed as seen in the business domain of the solution and not by technology, which means that the responsibility of a Feature layer module is defined by the intent of the module as seen by a business user and not by the underlying technology. Therefore, the module’s responsibility and naming should never be decided by specific technologies but rather by the module’s business value or business responsibility.

Discussion: For our feature modules, we aimed for single concrete features that are independent of each other. They may contain views, templates, controllers, renderings, configuration changes and related business logic code to tie it all together. The point is to always stick to the rule: “Classes that change together are packaged together”.

When building feature modules, it’s also very handy to think about the feature modules removal as you build it. Keep asking yourself how easy would it be to roll back this module and what would I need to do. Doing so will help you to keep those dependencies under control.

Foundation Definition: The lowest level layer in Helix is the Foundation layer, which as the name suggests forms the foundation of your solution. When a change occurs in one of these modules it can impact many other modules in the solution. This mean that these modules should be the most stable in your solution in term of the Stable Dependencies Principle.

 

Discussion: We found that our foundation modules usually consist of frameworks or code that provide a structural functionality to support the web application as a whole. Each foundation module may be used by multiple feature modules to provide them with the support they need to run properly. Our foundation modules contain API calls, configuration, ORM structures (Glass Mapper), initialisation code, interfaces and abstract base classes.

An important point is that unlike feature layer modules, the foundation layer modules can have dependencies to other foundation layer modules. If this was not the case it would be very difficult to construct the foundation layer in the first place.

For the most part, the team can make some fairly quick decisions about what goes where in the initial project planning. And what goes where is fairly obvious after you get familiar with the habitat example project. The main dilemma you’re going to encounter is around where your repositories and services (key business logic) might need to sit.

Business LogicWhat goes where! Help!

Let’s consider the definitions above, they seem straightforward enough. However, in agile projects where things may change rapidly or requirements are not immediately clear (which happens a lot), you’re inevitably going to need to make some judgment calls.

What am I talking about with the above statement, well let’s say one developer codes up a feature module at the beginning of that project. At first, it seems like that particular portion of code is only required by that particular feature. Down the track a requirement surfaces whereby the same business logic needs to be used in another Feature module. Helix rules dictate:

  • Classes that change together are packaged together.
  • No dependencies between feature projects should exist.

A lesser developer may be tempted at this point to duplicate the code in both feature modules to get the job done quickly. This, however, breaks some fairly important fundamental coding standards that many of us try to stick to. Step back to consider the technical debt that duplicate code leave behind vs dependencies between your Helix feature modules.

The solution to this dilemma; it’s time to refactor that feature logic and move it into the foundation layer. After which any feature modules that needs to reference the same code can reference it in the foundation layer.

Remember “with great power comes great responsibility” and this is especially true when touching code in the foundation layer. The code you touch in the foundation layer should be highly stable and well tested as the helix guidelines suggest.

Was the original decision a mistake?

On the flip side of the coin its worth considering that it wasn’t a mistake to put that piece of code in the feature layer to start with. Technically if no one else needed to use the code at the time and it was reasonably unforeseen that anyone else would need to use it, then it probably was the correct call.

Accept that things may change

The team members on your helix project will need to be flexible and accepting of change. I think it’s worth being prepared for some open discussions within your team about what goes in the foundation layer and what goes in the feature layer. It’s certainly going to be open to interpretation and a topic of debate. A team that can work together and be open to a change of direction within their code structure will help the code base stay within the helix guidelines as the project evolves.

Good luck!

 

Helix Template Inheritance

During Sitecore Helix development it’s important to understand the role that template inheritance plays in keeping your dependencies in check.

The current conventions shown in the Habitat example site is to:

  • Create your template fields in modules within the foundation and feature layers with templates starting with a ‘_’.
  • featureinheritance
  • Inside the project layer, create appropriate templates that inherit from the feature and foundation layer templates. inheritanceproject

Each project layer template should inherit from one or more feature / foundation layer templates.

We believe the benefits of sticking to this approach are as follows:

  • Your project layer templates can be composed of template fields from multiple modules in other layers. With the potential for page templates to contain the functionality from multiple modules if need be.
  • Your content tree does not directly rely on feature and foundation layer templates making their removal down the track easier if need be.

Data Source Locations

Helix introduces the concept of Data Source Locations which are documented on this page.

datasourcelocations

Data source locations are very useful for supporting multi-tenant, multi-site solutions. You don’t have to use them, however using them does future proof your Helix solution with the ability to add additional tenants down the track.

The datasource location and template resolution have been extended in the Habitat project. This means that it is also possible to define datasource templates and locations for each site, in addition to on the rendering itself. This is done through an extension of the getRenderingDatasource pipeline and the addition of a site: prefix to the Datasource Location field.

Site:prefix

datasource-location

The above IFrame rendering uses the syntax site:iframe which via the getRenderingDatasource pipeline will look for a data source location within the site called ‘iframe’.

This is great for multi-tenant situations but it also means that the helix dependency rules are not broken. Pointing the data source location field of your rendering to a folder within a website technically breaks the Helix dependency rules. This is a very important point and one that might be missed on a first pass through the Helix documentation.

The trap of using Feature Templates

Developers that are new to helix may not realise that any feature layer modules should have a matching template in the website layer that inherits from the feature layer.

datasource-location2

A follow-on effect from this is that you may skip the creation of a project layer template altogether and simply use the feature layer template in the “Datasource Template” field.

Technically the above dependency is not incorrect. Habitat has one more surprise install for you, however. Once you start to add content blocks to your page based on the data source locations, your insert options for the Local data sources folder start to get automatically populated.

InsertOptions

So for each unique component, you add to the page via Experience Explorer your going to get the template of that component in the insert options.

once again this dependency is not technically incorrect, although it will probably have your testers asking why strangely named templates are showing up in the insert options. The major drawback we could think (as mentioned earlier in the article) is that your content items will be highly dependent on that particular feature module.

  • You will have multiple dependencies between content items and the feature modules.
  • Attempting to remove the feature layer will disrupt all of those content items directly.

On the flip side using the project template instead, means we have a single (or reduced) point of removal for that feature module within the project templates.

The main takeaway we took by running into the above mistake (using the feature template in our data source locations) was that we should be pointing the data source locations at a project template instead.

  • Always create a project layer template that will inherit from a feature/foundation module template.
  • Ideally content items shouldn’t reference feature layer templates directly, even though this doesn’t break the helix dependency rules. 

 

WFFM 170518 Tips & Tricks

Below you will find some tips and tricks that we found when implementing WFFM for Sitecore 8.2 Update 4.

Version: Web Forms for Marketers 8.2 rev. 170518

  1. You can’t mix Web Forms and the MVC Form rendering
    • 49048 16:14:46 ERROR Rendering control {F2CCA16D-7524-4E99-8EE0-78FF6394A3B3} not found for '/sitecore/content/Home'. Item id: {110D559F-DEA5-42EA-9C1C-8A5DF7E70EF9}, database: master.
    • I hit this problem on a vanilla install of 8.2.4 with WFFM installed. If you try to add this form to the homepage item via the MVC form rendering provided expect this problem.
    • Cause: The sample layout provided for the default homepage uses a .aspx page.
    • Solution: You need to create a layout using MVC and assign to the page you want to add the MVC form rendering to.
  2. SIM (sitecore instance manager) hangs when install Sitecore 8.2.4 and Web Forms for Marketers 8.2 rev. 170518
    • Cause: We are not sure of the exact cause but installing this package with SIM results in the process crashing and burning.
    • Solution:  Install the Sitecore 8.2.4 with SIM and once up and running install the WFFM via the package manager in Sitecore instead.
  3. If your site does not have xDB available, you may want to use an SQL data store for your forms.
    • Not every site requires xDB to be turned on and the additional hosting and license cost may force your hand on this one.
    • If you want to revert back to the SQL database. Locate the DB files installed in the root of your website in the Data folder. Attach the DB’s to your SQL server.
      • The database should contain 2 x tables. WFFM SQL DB
    • Add the database connection string to the file connectionstring.config in the folder: App_Config
      • If your building with Helix create a transform file in your Foundation.Forms project.
      • foundationforms
      • <?xml version="1.0" encoding="utf-8"?>
        <connectionStrings xmlns:xdt="http://schemas.microsoft.com/XML-Document-Transform">
         <add name="wfm"
         connectionString="Data Source=WINDEV;Initial Catalog=Sitecore_Wffm;Integrated Security=False;User ID=sa;Password=itsasecret"
         xdt:Transform="Insert"/>
        </connectionStrings>

         

    • Create a patch config file to turn on sqlFormsDataProvider:
      <configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
       <sitecore>
       <wffm>
       <analytics>
       <formsDataProvider patch:instead="*[@ref='/sitecore/wffm/analytics/analyticsFormsDataProvider']" ref="/sitecore/wffm/analytics/sqlFormsDataProvider"/>
       <sqlFormsDataProvider type="Sitecore.WFFM.Analytics.Providers.SqlFormsDataProvider, Sitecore.WFFM.Analytics">
       <param name="connectionStringName">wfm</param>
       </sqlFormsDataProvider>
       </analytics>
       </wffm>
       </sitecore>
      </configuration>

       

    • Not that when using the SQL Data Provider the tab “Detailed reports” will not work as it requires xDB
    • Image_2017-07-19_09.54.24_AM
  4. If your using the Send Mail Message action, located here: /sitecore/system/Modules/Web Forms for Marketers/Settings/Actions/Save Actions/Send Email Message
    • Your going to want to overwrite the default mail server settings before deploying.
    • Create a patch config file for these settings
    • <configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
       <sitecore>
       <settings>
       <setting name="MailServer" value="smtp.sendgrid.net" />
       <!-- MAIL SERVER USER
       If the SMTP server requires login, enter the user name in this setting
       -->
       <setting name="MailServerUserName" value="apikey" />
       <!-- MAIL SERVER PASSWORD
       If the SMTP server requires login, enter the password in this setting
       -->
       <setting name="MailServerPassword" value="SG.sdsdadXXCASFDa3423dfaXXXXXXXXXXXXXXXXXXXX" />
      
      <setting name="MailServerPort" value="25"/>
       </settings>
       </sitecore>
      </configuration>
    • This is handy in Helix setups.
    • If your using Azure why not take advantage of the Send Grid free tier. You can send a bunchof emails for free.
    • Also note that on the Send Email Message action you will need to set the host as well.
    • hostparameter

 

References:

Setting up Azure Search in Sitecore Helix

This applies to those setting up Azure Search to work with a Sitecore Helix architecture based on the Habitat example and using Sitecore 8.2 Update 3.

Depending on your use case Azure Search is a smart option if you’re hosting in the cloud. The main upside it likely has over SOLR is that its run as PaaS and you don’t need to delve into the SOLR setting files in order to enact scaling and replication. Nor does it require a true Virtual Machine running tomcat, that you may at times need to know how to administer. I’m not suggesting that SOLR isn’t a great project (it is) but it does have a learning curve if you need to tweak things under the hood. Azure Search also has its limitations as listed on this page.

If you’re following the setup instructions (like I was) there is a fairly lengthy step that involves you switching over configuration files. Basically, on a default installation, you need to disable/delete all Lucene file system based configuration files and enable a number of Azure configuration files for Azure Search PaaS.

Using Helix and gulp in Visual Studio all members of your team are probably going to need to replicate these setup steps (without doing anything manually in the running IIS developer instance).  So here are two tricks I used:

ConnectionStrings

Under your Foundation.Indexing module I placed a file called:

CaptureConnectionStrings

ConnectionString.config.transform

<?xml version="1.0" encoding="utf-8"?>

<connectionStrings xmlns:xdt="http://schemas.microsoft.com/XML-Document-Transform">
 <add name="cloud.search"
 connectionString="serviceUrl=https://mysearchinstance.search.windows.net;apiVersion=2015-02-28-Preview;apiKey=7CA4...5"
 xdt:Transform="Insert"/>
</connectionStrings>

 

Gulp Config Rename

In order to automate the deletion of all the Lucene config files I wrote the following Gulp task:

/*****************************
 https://doc.sitecore.net/sitecore_experience_platform/setting_up_and_maintaining/search_and_indexing/configure_azure_search#_Create_a_Search
 // If running on Azure Search we need to disable lucene default configs
*****************************/
gulp.task('Z-Search-Disable-Lucene-Configs', function () {

var root = config.websiteRoot + "/App_Config/Include/";
 var socialRoot = config.websiteRoot + "/App_Config/Include/social/";
 var listManagementRoot = config.websiteRoot + "/App_Config/Include/ListManagement/";
 var fxmRoot = config.websiteRoot + "/App_Config/Include/FXM/";
 var testingRoot = config.websiteRoot + "/App_Config/Include/ContentTesting/";

// Main root config folder
 var roots = [root + "Sitecore.ContentSearch.Lucene.DefaultIndexConfiguration.config",
 root + "Sitecore.ContentSearch.Lucene.Index.Analytics.config",
 root + "Sitecore.ContentSearch.Lucene.Index.Core.config",
 root + "Sitecore.ContentSearch.Lucene.Index.Master.config",
 root + "Sitecore.ContentSearch.Lucene.Index.Web.config",

root + "Sitecore.Marketing.Lucene.Index.Master.config",
 root + "Sitecore.Marketing.Lucene.Index.Web.config",
 root + "Sitecore.Marketing.Lucene.IndexConfiguration.config",
 root + "Sitecore.Marketing.Definitions.MarketingAssets.Repositories.Lucene.Index.Master.config",
 root + "Sitecore.Marketing.Definitions.MarketingAssets.Repositories.Lucene.Index.Web.config",
 root + "Sitecore.Marketing.Definitions.MarketingAssets.Repositories.Lucene.IndexConfiguration.config",

root + "Sitecore.Speak.ContentSearch.Lucene.config",
 root + "Sitecore.Marketing.Definitions.MarketingAssets.Repositories.Lucene.Index.Web.config",
 root + "Sitecore.Marketing.Definitions.MarketingAssets.Repositories. Lucene.IndexConfiguration.config",

root + "Sitecore.Speak.ContentSearch.Lucene.config",
 root + "Sitecore.ContentSearch.Lucene.DefaultIndexConfiguration.Xdb.config"
 
 ];




var fxmFiles = [
 fxmRoot + "Sitecore.FXM.Lucene.DomainsSearch.DefaultIndexConfiguration.config",
 fxmRoot + "Sitecore.FXM.Lucene.DomainsSearch.Index.Master.config",
 fxmRoot + "Sitecore.FXM.Lucene.DomainsSearch.Index.Web.config",
 ];




var listManFiles = [
 listManagementRoot + "Sitecore.ListManagement.Lucene.Index.List.config",
 listManagementRoot + "Sitecore.ListManagement.Lucene.IndexConfiguration.config",
 ];




var socialsFiles = [
 socialRoot + "Sitecore.Social.Lucene.Index.Master.config",
 socialRoot + "Sitecore.Social.Lucene.Index.Web.config",
 socialRoot + "Sitecore.Social.Lucene.IndexConfiguration.config",
 socialRoot + "Sitecore.Social.Lucene.Index.Analytics.Facebook.config"
 ];




var testingFiles = [
 testingRoot + "Sitecore.ContentTesting.Lucene.IndexConfiguration.config",
 ];

return Promise.all([
 new Promise(function (resolve, reject) {

gulp.src(roots).pipe(rename(function (path) {
 path.extname = ".config.disabled";
 })).pipe(gulp.dest(root));




gulp.src(fxmFiles).pipe(rename(function (path) {
 path.extname = ".config.disabled";
 })).pipe(gulp.dest(fxmRoot));

gulp.src(listManFiles)
 .pipe(rename(function (path) {
 path.extname = ".config.disabled";
 })).pipe(gulp.dest(listManagementRoot));

gulp.src(socialsFiles).pipe(rename(function (path) {
 path.extname = ".config.disabled";
 })).pipe(gulp.dest(socialRoot));

gulp.src(testingFiles).pipe(rename(function (path) { path.extname = ".config.disabled"; }))
 .pipe(gulp.dest(testingRoot));

}),
 new Promise(function (resolve, reject) {
 del(roots, { force: true });
 del(fxmFiles, { force: true });
 del(listManFiles, { force: true });
 del(socialsFiles, { force: true });
 del(testingFiles, { force: true });
 })
 ]).then(function () {
 // Other actions
 del(roots, { force: true });
 del(fxmFiles, { force: true });
 del(listManFiles, { force: true });
 del(socialsFiles, { force: true });
 del(testingFiles, { force: true });
 });
 
});

 

In order to automate the renaming of all the Azure config files I wrote the following Gulp task:

 

gulp.task('Z-Search-Enable-Azure-Configs', function () {

var root = config.websiteRoot + "/App_Config/Include/";
 var socialRoot = config.websiteRoot + "/App_Config/Include/social/";
 var listManagementRoot = config.websiteRoot + "/App_Config/Include/ListManagement/";
 var fxmRoot = config.websiteRoot + "/App_Config/Include/FXM/";
 var testingRoot = config.websiteRoot + "/App_Config/Include/ContentTesting/";

// Main root config folder
 var roots = [root + "Sitecore.ContentSearch.Azure.DefaultIndexConfiguration.config.disabled",
 root + "Sitecore.ContentSearch.Azure.Index.Analytics.config.disabled",
 root + "Sitecore.ContentSearch.Azure.Index.Core.config.disabled",
 root + "Sitecore.ContentSearch.Azure.Index.Master.config.disabled",
 root + "Sitecore.ContentSearch.Azure.Index.Web.config.disabled",

root + "Sitecore.Marketing.Azure.Index.Master.config.disabled",
 root + "Sitecore.Marketing.Azure.Index.Web.config.disabled",
 root + "Sitecore.Marketing.Azure.IndexConfiguration.config.disabled",
 root + "Sitecore.Marketing.Definitions.MarketingAssets.Repositories.Azure.Index.Master.config.disabled",
 root + "Sitecore.Marketing.Definitions.MarketingAssets.Repositories.Azure.Index.Web.config.disabled",
 root + "Sitecore.Marketing.Definitions.MarketingAssets.Repositories.Azure.IndexConfiguration.config.disabled"
 ];

gulp.src(roots) .pipe(rename(function (path) { path.extname = ""; path.basename = path.basename.replace(".disabled");
 })).pipe(gulp.dest(root));

// Testing files

var testingFiles = [
 testingRoot + "Sitecore.ContentTesting.Azure.IndexConfiguration.config.disabled",
 ];

gulp.src(testingFiles).pipe(rename(function (path) { path.extname = ""; path.basename = path.basename.replace(".disabled"); }))
 .pipe(gulp.dest(testingRoot));
 //del(testingFiles, { force: true });

// FXM files

var fxmFiles = [
 fxmRoot + "Sitecore.FXM.Azure.DomainsSearch.DefaultIndexConfiguration.config.disabled",
 fxmRoot + "Sitecore.FXM.Azure.DomainsSearch.Index.Master.config.disabled",
 fxmRoot + "Sitecore.FXM.Azure.DomainsSearch.Index.Web.config.disabled",
 ];

gulp.src(fxmFiles) .pipe(rename(function (path) { path.extname = ""; path.basename = path.basename.replace(".disabled");
 })).pipe(gulp.dest(fxmRoot));
 
 var listManFiles = [
 listManagementRoot + "Sitecore.ListManagement.Azure.Index.List.config.disabled",
 listManagementRoot + "Sitecore.ListManagement.Azure.IndexConfiguration.config.disabled",
 ];
 
 gulp.src(listManFiles)
 .pipe(rename(function (path) { path.extname = ""; path.basename = path.basename.replace(".disabled");
 })).pipe(gulp.dest(listManagementRoot));

var socialsFiles = [
 socialRoot + "Sitecore.Social.Azure.Index.Master.config.disabled",
 socialRoot + "Sitecore.Social.Azure.Index.Web.config.disabled",
 socialRoot + "Sitecore.Social.Azure.IndexConfiguration.config.disabled"
 ];

gulp.src(socialsFiles).pipe(rename(function (path) {
 path.extname = ""; path.basename = path.basename.replace(".disabled");
 })).pipe(gulp.dest(socialRoot));
});

 

I hope this saves you and your team some time when switching over to Azure Search on your Sitecore Helix project. Please leave us some comments if you have some more tips along these lines.