Friday, 28 February 2014

A Switching Link Provider in Sitecore

Image by by Diorama Sky
This post demonstrates a way of allowing Sitecore to dynamically select a link provider depending on the current context. It's partly inspired by an article written by Craig Taylor about developing multi-site Sitecore instances, with different teams (potentially different companies) working on each site. In situations such as that, I simply don't think it's possible to proceed unless you adopt strategies like the one I outline here.


Background

A link provider is the means by which Sitecore generates URLs for items. Whenever you use an sc:link control, call the LinkManager, or render a Rich Text field that contains hyperlinks, then Sitecore calls the GetItemUrl method of its link provider to construct an appropriate URL.

In non-trivial Sitecore instances you will often need to customize the way Sitecore constructs URLs. For example, if a site has a large product range they might want all product URLs to be constructed as http://mysite.com/products/flugel-binder, but store the items outside of the site's Home node in a more practical structure.

In more complex scenarios such a multi-site instances you may have several custom requirements for the way links should be assembled. In this situation, it becomes a good idea to split the code responsible for these different requirements into more manageable chunks. You can then let Sitecore decide for itself which of these to use in any given context - a switching provider.

Sitecore already has the concept of Switching Providers, which can be implemented in situations where you have complex security requirements. You can set up multiple membership, role and profile providers. At runtime the switching provider can use them interchangeably, but the client code will be unaware that this process is taking place.

Down to Business.

So lets adapt the switching security provider concept in Sitecore for use with links. In this example I'm using the context site to decide, but the decision making logic is relatively simple and can be easily changed to fit your purposes. Starting with the the linkManager node of the web.config, we'll add:
  1. a reference to our new switching provider (switcher), and set it as the default.
  2. some site specific link providers  (ssp1 and ssp2)  for switcher to use 
  3. a new node (siteMappings), which associates a site with a link provider
<linkmanager defaultprovider="switcher">

  <providers>
    <add name="sitecore" type="Sitecore.Links.LinkProvider, Sitecore.Kernel"/>
    <add name="switcher" type="MySite.SwitchingLinkProvider, MySite" />
    <add name="ssp1" type="MySite.FirstLinkProvider, MySite" />
    <add name="ssp2" type="MySite.SecondLinkProvider, MySite" />
  </providers>

  <sitemappings fallback="sitecore">
    <mapping provider="ssp1" site="firstSite" />
    <mapping provider="ssp2" site="secondSite" />
  </sitemappings>

</linkmanager>
The switching provider class inherits from Sitecore's base LinkProvider. We have 2 private instance variables, _siteMappings and _fallbackMapping, which the constructor populates from the siteMappings node of the web.config.
public class SwitchingLinkProvider : LinkProvider 
{
    private Dictionary _siteMappings;
    private String _fallbackMapping;

    public SwitchingLinkProvider()
    {      
        _siteMappings = new Dictionary();
        
        // Get the site mapping node from the config.
        XmlNode mappingNodes = Factory.GetConfigNode("linkManager/siteMappings");
        Assert.IsNotNull(mappingNodes, "mappingNodes");

        // Store the name of the provider to use if a site is not mapped
        Assert.IsNotNull(mappingNodes.Attributes["fallback"], "fallback");
        _fallbackMapping = mappingNodes.Attributes["fallback"].Value;

        // Store the provider name for each mapped site
        foreach (XmlNode node in mappingNodes)
        {
            Assert.IsNotNull(node.Attributes["site"], "site");
            String siteName = node.Attributes["site"].Value.ToLower();
            Assert.IsFalse(_siteMappings.ContainsKey(siteName), "duplicate site");   
            Assert.IsNotNull(node.Attributes["provider"], "provider");
            String providerName = node.Attributes["provider"].Value.ToLower();
                   
            _siteMappings.Add(siteName, providerName);        
        }        
    }
Now we have to work out which provider to use depending on the current site:
private LinkProvider GetContextProvider()
{
    String siteName = Sitecore.Context.Site.Name.ToLower();
    
    // If we have a mapping for this site, return the correct provider        
    if (_siteMappings.ContainsKey(siteName))
    {
        var providerName = _siteMappings[siteName];
        Assert.IsNotNullOrEmpty(providerName, "providerName");
        var siteProvider = LinkManager.Providers[providerName];
        Assert.IsNotNull(siteProvider, "siteProvider");
        return siteProvider;
    }

    // If we couldn't find a mapping, then map 
    // the fallback provider for next time
    _siteMappings.Add(siteName, _fallbackMapping);
    var fallbackProvider = LinkManager.Providers[_fallbackMapping];
    Assert.IsNotNull(fallbackProvider, "fallbackProvider");
    return LinkManager.Providers[_fallbackMapping];
}
All that remains is to use GetContextProvider() to wire up the methods of our switching provider to those of the delegated site-specific provider:
public override bool AddAspxExtension 
{
    get { return GetContextProvider().AddAspxExtension; }
}

public override bool AlwaysIncludeServerUrl 
{
    get { return GetContextProvider().AlwaysIncludeServerUrl; }
}

public override bool EncodeNames 
{
    get { return GetContextProvider().EncodeNames; }   
}

public override LanguageEmbedding LanguageEmbedding 
{
    get { return GetContextProvider().LanguageEmbedding; }
}

public override LanguageLocation LanguageLocation 
{
    get { return GetContextProvider().LanguageLocation; }
}

public override bool LowercaseUrls 
{
    get { return GetContextProvider().LowercaseUrls; }
}

public override bool ShortenUrls 
{
    get { return GetContextProvider().ShortenUrls; }
}
        
public override bool UseDisplayName 
{
    get { return GetContextProvider().UseDisplayName; }     
}

public override string ExpandDynamicLinks(string text, bool resolveSites)
{
    return GetContextProvider().ExpandDynamicLinks(text, resolveSites);
}

public override UrlOptions GetDefaultUrlOptions()
{
    return GetContextProvider().GetDefaultUrlOptions();
}

public override string GetDynamicUrl(Item item, LinkUrlOptions options)
{
    return GetContextProvider().GetDynamicUrl(item, options);
}

public override string GetItemUrl(Item item, UrlOptions options)
{
    return GetContextProvider().GetItemUrl(item, options);
}

public override bool IsDynamicLink(string linkText)
{
    return GetContextProvider().IsDynamicLink(linkText);
}

public override DynamicLink ParseDynamicLink(string linkText)
{
    return GetContextProvider().ParseDynamicLink(linkText);
}

public override RequestUrl ParseRequestUrl(HttpRequest request)
{
    return GetContextProvider().ParseRequestUrl(request);
}
}
Some related articles