Monday, 20 October 2014

Experimenting with the Sitecore Site Provider

Recently I've been experimenting with the way Sitecore detects what sites are included in an instance. When Sitecore starts up, by default  it looks in the “Sites” node of the web.config and builds up a list of in-memory Site objects which are kept for the lifetime of the application. I was intrigued to see that, like many Sitecore subsystems, a provider approach is used. This means we’re able to replace the default behaviour with one of  our own.

EDIT: Weirdly, Sitecore's own Integration Solutions Team published a blog article on this exact subject a few hours before me. I can only conclude that this is no mere "coincidence". They clearly hacked my laptop and stole my idea. They will be hearing from my lawyers...

It occurred to me that many of the attributes that you add to Site definition elements in the web.config could be inferred so long you can identify the “Site Root” item. You don’t need to specify most of the information explicitly because it’s relative to that one item. For example, when I create a new site in the content tree, the home page is typically a child item of the root item.

I wondered if it was possible to dispense with “sites” config node altogether, and just use data obtained from Sitecore items. If this is possible, that means we could give more freedom to Sitecore users to create new sites themselves without involvement from a developer.

I decompiled Sitecore’s default ConfigSiteProvider, and after some investigation I came up the idea of finding all the items with a "Site root" template, using them to generate Sitecore site objects. I decided that it would be useful to also include the base behaviour of the ConfigSiteProvider, so after getting our root items, we should also get data from the config file
class CustomSiteProvider : ConfigSiteProvider
{
    private object _lock = new object();
    private SiteCollection _sites;
    private SafeDictionary _siteDictionary;

    public override Site GetSite(string siteName)
    {
        InitializeSites();

        // first we look in our custom collection of sites
        if (_siteDictionary.ContainsKey(siteName))
            return _siteDictionary[siteName];

        // and fallback to the sites from config if we didn't find one
        return base.GetSite(siteName);
    }

    public override SiteCollection GetSites()
    {
        InitializeSites();
            
        // we get our custom collection of sites
        var allSites = new SiteCollection();
        allSites.AddRange(_sites);

        // then append the sites obtained from config
        var configSiteDefintitions = base.GetSites();
        allSites.AddRange(configSiteDefintitions);

        return allSites;
    }

    private void InitializeSites()
    {
        // check if we've already built our site dictionary
        if (_siteDictionary != null)
            return;

        // if not, then get on with it
        lock (_lock)
        {
            if (_siteDictionary != null)
                return;

            _sites = GetSitesFromSiteRootItems();
            _siteDictionary = GetSiteDictionary(_sites);
        }
    }

    private SiteCollection GetSitesFromSiteRootItems()
    {
        // This is where you get a list of items based on the "Root Item" 
        // template. In my test environment I used a Lucene search, but you 
        // can do this in whatever way you prefer.
        throw new NotImplementedException();
        
        // Then for each item, we generate a site.
        var collection = new SiteCollection();
        foreach(item in rootItems)
        {
            var site = GenerateSite(item);
            collection.Add(site);
        }

        return collection;
    }

    private SafeDictionary GetSiteDictionary(SiteCollection sites)
    {
        // This provides a way of looking up a site by name.
        var dict = new SafeDictionary(StringComparer.OrdinalIgnoreCase);

        foreach (Site site in sites)
        {
            if (_siteDictionary.ContainsKey(site.Name))
                continue;

            _siteDictionary.Add(site.Name, site);
        }     
   
        return dict;
    }

    private Site GenerateSite(Item item)
    {
        // We populate the 'properties' dictionary with the relavant
        // information. This would normally be the attributes in your
        // config site definition, but we're using the site root Item 
        // as the source.
        Sitecore.Diagnostics.Assert.ArgumentNotNull(item, "item");

        var site = new Site(item.Name);
        site.Properties.Add("virtualFolder", "/" + item.Name);
        site.Properties.Add("physicalFolder", "/" + item.Name);
        site.Properties.Add("rootPath", item.Paths.FullPath);

        var startItem = item.Children.
                        FirstOrDefault(child => 
                        child.TemplateID.ToString() == "{my-homepage-template-id}");

        site.Properties.Add("startItem", "/" + startItem.Name);
        site.Properties.Add("name", item.Name);

        // This information can't be inferred, so we get 
        // it from fields on the root item.
        site.Properties.Add("hostName", item["Host name"]);
        site.Properties.Add("targetHostName", item["Target host name"]);
        site.Properties.Add("database", item["database"]);
        site.Properties.Add("domain", item["domain"]);
        site.Properties.Add("cacheHtml", item["Cache html"]);
        site.Properties.Add("htmlCacheSize", item["Html cache size"]);
        site.Properties.Add("registryCacheSie", item["Registry cache size"]);
        site.Properties.Add("viewStateCacheSie", item["View state cache size"]);
        site.Properties.Add("xslCacheSize", item["Xsl cache size"]);
        site.Properties.Add("enableAnalytics", item["Enable analytics"]);
        site.Properties.Add("allowDebug", item["Allow debug"]);
        site.Properties.Add("allowPreview", item["Allow preview"]);
        site.Properties.Add("enableWebEdit", item["Enable web edit"]);
        site.Properties.Add("enableDebugger", item["Enable debugger"]);
        site.Properties.Add("disableClientData", item["Disable client data"]);
 
        return site;
    }
}
Did it work?
Well, kind of. When using this approach, the sites do get built up as expected and the resulting URL’s do resolve correctly. However, as far as I can tell Sitecore only goes through this process at the very beginning of the application. If you add a new site root item, the only way to register it is to restart the application (which sort of defeats the object). I’d be interested to see if any Sitecore devs out there can come up with a solution that allows me to register a new site while the application is still running.

Another thing to note is that the provider needs to be set up in a way that conforms to a specific usage. You would have to adapt the code to meet the specific requirements of each individual implementation.

What do you think? Are there any other good use cases for creating a custom site provider?