Thursday, 28 November 2013

Create Site Specific Pipeline Processors for Sitecore

This post outlines a method of selecting different processors in the httpRequestBegin pipeline depending on the context site. (Edit: John West has now expanded on this idea, with an approach that does not require a 'parent' processor)


Whenever I write a Sitecore pipeline processor, I put far too much effort in to restricting the circumstances under which the code should run. More often than not this includes preventing it from being invoked by Sitecore's internal sites (Shell, Admin, Login etc).

In a multi-site Sitecore instance things start to get a little muddled. Now, in addition to the internal sites, you also need to think about how your processor should behave for each of your public sites. All of this can lead to nasty looking code like the following:

var site = Sitecore.Context.Site.Name;
if(site == "admin" || site == "shell" || == "site3")
    return;

CommonCode();

if(site == "site1")
    Site1SpecificCode();
else if(site == "site2")
    Site2SpecificCode();
else if(site == "site4" || site == "site5")
    Site4And5SpecificCode();

This code is not very readable and would probably need quite a bit of fine tuning to get it right. Faulty code in a processor can completely break all of your sites, so the added complexity isn't ideal.

I wanted to create the means to remove the conditional logic introduced when managing multiple sites, and work with processors which are clear and focused in their purpose. So I wrote a simple "selective processor" class which acts as a wrapper for other processors, delegating it's responsibilities depending on the context site.

The web.config snippet below shows how this would work. We have a scenario where Sitecore needs to perform redirects using varying logic on several different sites. There are three processors, each with a discrete purpose - Site1Redirector, Site2Redirector, GenericRedirector.

<processor type="Processors.SelectiveProcessor, MyAssembly">
  <cleverRedirectors hint="raw:Add">
    <redirector site="site1" type="Processors.Site1Redirector, MyAssembly"/>
    <redirector site="site2" type="Processors.Site2Redirector, MyAssembly"/>
    <redirector site="site4" type="Processors.GenericRedirector, MyAssembly"/>
    <redirector site="site5" type="Processors.GenericRedirector, MyAssembly"/>
  </cleverRedirectors>
</processor>

The SelectiveProcessor contains the arbitrarily named grouping element cleverRedirectors. The hint="raw:Add" attribute instructs Sitecore to make use of any child elements of cleverRedirectors in SelectiveProcessor.

The redirector elements (also arbitrarily named) make the connection between a site and a processor. At runtime the Sitecore context site name is paired up with the value supplied in the site attribute. The class specified in the type attribute of the matched element is then instantiated.

Hopefully the the example above demonstrates how this approach provides flexibility in applying site-conditional logic while also keeping the purpose of a processor simple and clear:
  • Sites 1 and 2 have their own custom processors.
  • Site 3 does not have redirect processor at all.
  • Sites 4 and 5 share a common processor.
The SelectiveProcessor class can be used as many times as is necessary within the pipeline, but must appear after the SiteResolver - If you don't know which site you're on, you can't choose an appropriate processor.
Here's the code for the class:

public class SelectiveProcessor : HttpRequestProcessor
{
    private Dictionary<String, String> _processors;

    public SiteSpecificProcessor()
    {
        _processors = new Dictionary<String, String>();
    }

    public void Add(XmlNode node)
    {
        Assert.IsNotNull(node.Attributes["site"],"Attribute: site");
        Assert.IsNotNull(node.Attributes["type"],"Attribute: type");

        var siteName = node.Attributes["site"].Value.ToLower();
        var typeName = node.Attributes["type"].Value;

        var type = Type.GetType(typeName);
        Assert.IsNotNull(type, "No such type: " + typeName);

        var typeIsUsable = typeof(HttpRequestProcessor).IsAssignableFrom(type);
        Assert.IsTrue(typeIsUsable, "Not assignable from " + type.Name);

        _processors.Add(siteName, typeName);
    }

    public override void Process(HttpRequestArgs args)
    {
        // If we don't know the current site we can't choose a suitable processor.
        if (Sitecore.Context.Site == null)
            return;

        var contextSite = Sitecore.Context.Site.Name.ToLower();

        // We only want to continue if the current site has been specified.
        if(!_processors.ContainsKey(contextSite))
            return;

        // Get the type from the fully qualified name.
        var typeName = _processors[contextSite];
        var type = Type.GetType(typeName);

        // Instantiate the selected processor and call its Process method.
        var processor = (HttpRequestProcessor)Activator.CreateInstance(type);
        processor.Process(args);
    }
}

For more information on how all this works read John West's blog posts on Pipelines and the Sitecore Configuration Factory.