Site-Specific Providers

Posted 02/27/2014 by Valerie Concepcion

For my first ever Sitecore project, I was asked to help build a content management solution that would support the provisioning of over 20,000 websites. Having no prior Sitecore project experience, I was unfazed by this number and got to work diligently coding sublayouts and page templates without really having any appreciation for what went into designing such a scalable solution. Fast-forward six years, and our team is again working on a site factory-type solution which must be capable of quickly churning out "highly configurable/customizable" sites.

A common pattern that Sitecore uses to support customization is to create a "manager" interface which delegates its method calls to a "provider" whose type is specified in the web.config file. A clear example of this pattern in use can be found in the LinkManager/LinkProvider, which allows us to reference methods like LinkManager.GetItemUrl(...) and swap out the underlying implementation without changing all our code.

<linkManager defaultProvider="sitecore">
  <providers>
     <clear/>
     <add name="sitecore" type="Sitecore.Links.LinkProvider, Sitecore.Kernel" addAspxExtension="true" alwaysIncludeServerUrl="false" encodeNames="true" languageEmbedding="asNeeded" languageLocation="filePath" lowercaseUrls="false" shortenUrls="true" useDisplayName="false"/>
  </providers>
</linkManager>

We've now adopted this pattern on many of our projects, but note that the LinkManager is a global interface, and thus the specified provider will apply to all sites in your solution.

So what if you want to allow each site in your solution to be configured with a different provider?

I mean, like, wouldn't it be nice if your config element looked like this??

<sites>
  <site patch:before="*[@name='website']"
          name="site-a"
          inherits="website"
          type="NTT.Business.Sites.Site, NTT.Business"
          rootPath="/sitecore/content/Site A"
          hostname="site-a.localhost.com"
          languageEmbedding="always"
          language="en"
          dictionaryDomain="Site A"
          enableCmsFramework="true">
     <param desc="name">$(name)</param>
     <param desc="site root path">$(rootPath)</param>
     <languageProvider type="NTT.Business.Providers.Language.DefaultLanguageProvider, NTT.Business" />
     <productProvider type="NTT.Business.Providers.Product.BucketsProductProvider, NTT.Business">
       <bucketRootPath>/sitecore/content/Site A/Home/Products</bucketRootPath>
     </productProvider>
  </site>
</sites>

But wait, it can!

First off, a big thanks to John West for writing this blog post on the Sitecore Configuration Factory. Believe it or not, you can read the above config to construct a custom Site object with local provider references in less than 10 lines of code (excluding braces).

public static NTT.Business.Sites.Site GetSite(string siteName)
{
     Assert.ArgumentNotNullOrEmpty(siteName, "siteName");
     var path = string.Format("sites/site[@name='{0}']", siteName);
     var config = Sitecore.Configuration.Factory.GetConfigNode(path);
     if (config != null)
     {
          return Sitecore.Configuration.Factory.CreateObject<Site>(config);
     }
     return null;
}

The above code, which is called from our custom SiteFactory class, will find a <site> element whose name matches the method input parameter and create an object of the type specified in the "type" attribute (i.e., NTT.Business.Sites.Site). The constructor of the Site class will be given the two <param> values, $(name) and $(rootPath), which are variables taken from the <site> attributes.

public class Site
{
     ...
     public Site(string name, string siteRootPath)
     {
          Assert.ArgumentNotNullOrEmpty(name, "name");
          Assert.ArgumentNotNullOrEmpty(siteRootPath, "siteRootPath");
          Name = name;
          _siteRootPath = siteRootPath;
     }
     ...
}

Finally, the Site class properties named "LanguageProvider" and "ProductProvider" will auto-magically be set to objects of the types specified on their respective "type" attributes. (Likewise, the "ProductProvider" string property named "BucketRootPath" will auto-magically be set to the value of the <bucketRootPath> element.)

public class Site
{
     ...
     public virtual ProductBaseProvider ProductProvider
     {
          get;
          set;
     }

     public virtual LanguageBaseProvider LanguageProvider
     {
          get;
          set;
     }
     ...
}

Note that the ProductBaseProvider and LanguageBaseProvider types are abstract classes which are implemented by the concrete types NTT.Business.Providers.Language.DefaultLanguageProvider and NTT.Business.Providers.Product.BucketsProductProvider. This means whatever type you specify on the provider elements will be expected to inherit from these base classes.

Now, to go the extra mile and provide developer-friendly access to the above objects, I've patched in a CustomSiteResolver processor that runs after the Sitecore.Pipelines.HttpRequest.SiteResolver to store a reference to my domain-specific Site object.

public class CustomSiteResolver : HttpRequestProcessor
{
     public override void Process(HttpRequestArgs args)
     {
          string siteName = Sitecore.Context.GetSiteName();
          if (!string.IsNullOrEmpty(siteName) && IsCmsFrameworkEnabled(siteName))
          {
               var site = SiteFactory.GetSite(Sitecore.Context.Site);
               if (site != null)
               {
                    DomainContext.Site = site;
               }
          }
     }
}

...and created a Site decorator class to serve as an interface to my provider methods.

public class ProductCatalog : SiteDecorator
{
     public ProductCatalog(Site site) : base(site)
     {
          Assert.ArgumentNotNull(site, "site");
          Assert.IsNotNull(site.ProductProvider, string.Format("ProductProvider cannot be null for site {0}", site.Name));
          base.ProductProvider = Site.ProductProvider;
     }

     public Models.Global.Product.Product GetProduct(string name)
     {
          return ProductProvider.GetProduct(name);
     }
}

All this...so that a young developer like myself (6 years ago) can use the following code in their sublayout and know that the site-specific product provider is in use.

var productCatalog = new ProductCatalog(DomainContext.Site);
var product = productCatalog.GetProduct(string name);
litProductName.Text = product.Name;

Don't take it for granted, son.

Share:

Archive

Syndication