Feature Toggles in Sitecore: Improve Performance

Last week, I gave an introduction on how you might implement a rudamentary version of Feature Toggles in Sitecore. If you have been around Sitecore long enough, you’ll notice that, if implemented that way in a high-volume, complicated site, that performance might be adversely affected. In this article, I’ll talk a little about what I did to improve the performance of feature toggles on a site with millions of hits per day. Two words: Indexing and Caching.

Storing Feature Toggles in the Index

If you recall in the previous post, the IFeatureToggleRepository implementation went directly to the Sitecore API (and thus, directly to the database) every time it was called. This is incredibly inefficient, especially given how relatively little the state of a feature toggle should change vs how often it’s called. So rather than have the repository go directly to the database through the API, let’s refactor it to hit the index instead.

First, let’s update the index configuration to start indexing our Toggleable Feature items.

<sitecore>
  <contentSearch>
    <configuration type="Sitecore.ContentSearch.ContentSearchConfiguration, Sitecore.ContentSearch">
      <indexes hint="list:AddIndex">
        <index id="sitecore_sxa_master_index" role:require="Standalone or ContentManagement">
          <locations hint="list:AddCrawler">
            <crawler type="Sitecore.ContentSearch.SitecoreItemCrawler, Sitecore.ContentSearch">
              <Database>master</Database>
              <Root>/sitecore/system/Settings/Foundation/Feature Toggles</Root>
            </crawler>
          </locations>
        </index>

        <index id="sitecore_sxa_web_index" role:require="Standalone or ContentDelivery">
          <locations hint="list:AddCrawler">
            <crawler type="Sitecore.ContentSearch.SitecoreItemCrawler, Sitecore.ContentSearch">
              <Database>web</Database>
              <Root>/sitecore/system/Settings/Foundation/Feature Toggles</Root>
            </crawler>
          </locations>
        </index>
      </indexes>
    </configuration>
  </contentSearch>
</sitecore>

In this example, I’m storing the Toggleable Feature items in the SXA indexes, but they can go wherever you like.

Next, let’s create an item that will be returned from the content search api that represents the item in the index.

public class FeatureToggleSearchResultItem : SearchResultItem
{
    [IndexField("Enabled")]
    public bool IsFeatureEnabled { get; set; }

    [IndexField("Components")]
    public string[] Components { get; set; }

    [IndexField("_template")]
    public string ShortTemplateId { get; set; }
}

Finally, let’s update the repository to search the index rather than go directly to the database through the API.

public class SitecoreFeatureToggleRepository : IFeatureToggleRepository
{
    public IEnumerable<FeatureToggleSearchResultItem> SearchFeatures()
    {
        IQueryable<FeatureToggleSearchResultItem> searchResults;

        using (var context = ContentSearchManager.GetIndex("sitecore_sxa_master_index").CreateSearchContext())
        {

            // Guid of the Toggleable Feature template
            searchResults = context.GetQueryable<FeatureToggleSearchResultItem>().Where(x => x.TemplateId == ID.Parse("{E6C0218B-0E24-4E3A-9AF0-101BBFA16B4F}"));
        }

        return searchResults.ToList();
    }
}

This takes care of storing and retrieving Toggleable Feature items in/from the Sitecore indexes.

Caching

Now that we’re not running directly to the database every time, how about we try to avoid running to the index every time?

Let’s set some cache configuration settings.

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <featureToggleCaching>
      <featureToggles type="Foundation.Caching.Configuration.CacheConfiguration, Foundation.Caching">
        <cacheName>FeatureToggles</cacheName>
        <cacheSize>10MB</cacheSize>
        <cacheKey>featureToggleList</cacheKey>
      </featureToggles>
    </featureToggleCaching>
  </sitecore>
</configuration>

Now the custom cache implementation:

public interface IFeatureToggleCache
{
    IEnumerable<FeatureToggle> GetFromCache();
    void SetInCache(IEnumerable<FeatureToggle> value);
}

public class SitecoreFeatureToggleCache : CustomCache, IFeatureToggleCache
{
    private readonly string _cacheKey;

    protected SitecoreFeatureToggleCache(ICacheConfiguration configuration) : base(configuration.CacheName, configuration.MaxSize)
    {
        _cacheKey = configuration.CacheKey;
    }

    public void SetInCache(IEnumerable<FeatureToggle> value)
    {
        SetObject(_cacheKey, value);
    }

    public IEnumerable<FeatureToggle> GetFromCache()
    {
        return (IEnumerable<FeatureToggle>)GetObject(_cacheKey);
    }
}

Next, register this cache implementation with the container.

public void Configure(IServiceCollection serviceCollection)
{
    serviceCollection.AddSingleton<IFeatureToggleCache>(x =>
    {
        var configFactory = x.GetService<ISitecoreConfigurationFactory>();
        var settings = configFactory.CreateObject<ICacheConfiguration>("featureToggleCaching/featureToggles");
        return new SitecoreFeatureToggleCache(settings);
    });
}

Now that we’ve implemented our custom cache, let’s refactor the implementation of IFeatureToggleProvider to pull/push from/to cache if possible, to really improve performance.

public class FeatureToggleProvider : IFeatureToggleProvider
{
    private readonly IFeatureToggleRepository _featureToggleRepository;
    private readonly IFeatureToggleCache _featureToggleCache;

    public FeatureToggleProvider(IFeatureToggleRepository featureToggleRepository, IFeatureToggleCache featureToggleCache)
    {
        _featureToggleRepository = featureToggleRepository ?? throw new ArgumentNullException(nameof(featureToggleRepository));
        _featureToggleCache = featureToggleCache ?? throw new ArgumentNullException(nameof(featureToggleCache));
    }

    public bool IsFeatureDisabled(string feature)
    {
        var features = GetFeatures();
        var isDisabled = !features?.FirstOrDefault(x => string.Equals(x.FeatureId, feature, StringComparison.InvariantCultureIgnoreCase))?.IsEnabled ?? false;
        return isDisabled;
    }

    public bool IsRenderingDisabled(string rendering)
    {
        var features = GetFeatures();
        var isDisabled = !features?.FirstOrDefault(x => x.RenderingIds != null && x.RenderingIds.Select(s => s.ToLower())
                            .Contains(rendering?.ToLower()))?.IsEnabled ?? false;

        return isDisabled;
    }

    private IEnumerable<ToggleableFeature> GetFeatures()
    {
        var cachedFeatureList = _featureToggleCache.GetFromCache();

        if (cachedFeatureList != null)
        {
            return cachedFeatureList;
        }

        var features = new List<ToggleableFeature>();

        var searchResults = _featureToggleRepository.SearchFeatures();

        if (searchResults.Any())
        {
            features.AddRange(searchResults.Select(item => new ToggleableFeature
            {
                FeatureId = item.Uri.ItemID.ToString(),
                IsEnabled = item["Enabled"] == "1",
                RenderingIds = ((MultilistField)item.Fields["Components"]).TargetIDs?.Select(x => ID.Parse(x).ToString()).ToList() ?? new List<string>()
            }));
        }
        _featureToggleCache.SetInCache(features);

        return features;
    }
}

Conclusion

These updates will significantly improve the performance of the feature toggles implented in Sitecore.

Happy Sitecore trails, my friend!

Leave a comment