Dependency Injection for Sitecore Pipelines and Pipeline Groups.

Dependency Injection for Sitecore Pipelines and Pipeline Groups.

If you know me, you are aware of my love of Simple Injector, and if you don't know me, I love Simple Injector. I wanted to use it as the container that resolved a JSS layoutService processor, but the tried and true method of using ref and factory attributes blew up in my face. This blog is the result of a mammoth debugging session that ran from about 1 am this morning to now.

Back when I first started to try and work out how best to use Simple Injector and Sitecore I uncovered the ref attribute for pipelines, it allowed you to use a DI Container adaptor to resolve types by instructing Sitecore to use a factory.

First you would define the Container Adaptor Factory like this:

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <factories>
      <factory id="FactoryContainerAdapter" type="DependencyInversion.ContainerAdapters.FactoryContainerAdapter, DependencyInversion"/>
    </factories>
  </sitecore>
</configuration>

Then when you register your processor, you would use the ref over the type attribute and include the factory pointing at the above defined Container Adaptor Factory.

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <pipelines>
      <preprocessRequest>
        <processor ref="DependencyInversion.Pipelines.ExamplePipeline, DependencyInversion" factory="FactoryContainerAdapter"/>
      </preprocessRequest>
    </pipelines>
  </sitecore>
</configuration>

You can see this code in an example project here. It is the supporting repository for a talk I give on using good DI Practices when developing in Sitecore.

However, over the last 24 hours, my world was turned on its head when using the above pattern, was trying to add a processor to the getLayoutServiceContext pipeline for a JSS project I am working on and wanted to extend this pipeline to include Site and Theme settings to the payloads that a React app consumes. If you are interested in more details on this check out the official documentation, as this is not the focus of this post.

A colleague of mine followed the documentation and created the processor and the include file, and I wanted to change the resolution of this type to use Simple Injector. Simple Injector has some advanced features, like aspect-oriented programming, that allows you to, in my opinion, write code that is more succinct and maintainable.

If you wanted to know more about my thoughts on why you should look at using Simple Injector in your Sitecore project, check out another blog post of mine: Two containers are better than one.

I added a Factory that would act as the Container Adaptor to the solution and updated the JssGetLayoutServiceContextProcessor configuration to use the ref and factory attributes, deployed the code and configuration to that Sitecore web root and refreshed my browser.

What was returned I did not expect, a 500 error. (Well maybe a I did expect it a bit ;)

12064 21:01:02 ERROR Exception during Layout Service RenderItem (configuration: jss, item: /)
Exception: System.Xml.XPath.XPathException
Message: 'types/SitecoreJss.Examples.ExampleContextExtension, SitecoreJss.Examples' has an invalid token.
Source: System.Xml
   at MS.Internal.Xml.XPath.XPathParser.ParseXPathExpresion(String xpathExpresion)
   at System.Xml.XPath.XPathExpression.Compile(String xpath, IXmlNamespaceResolver nsResolver)
   at System.Xml.XPath.XPathNavigator.Select(String xpath)
   at System.Xml.XmlNode.SelectNodes(String xpath)
   at Sitecore.Pipelines.CorePipelineFactory.GetObjectNode(String objectName, XmlNode groupNode)
   at Sitecore.Pipelines.CorePipelineFactory.GetObject(String groupName, String pipelineName, String objectName, XmlNode groupNode)
   at Sitecore.Pipelines.CorePipelineFactory.GetProcessorObject(XmlNode processorNode)
   at Sitecore.Pipelines.CoreProcessor.GetMethod(Object[] parameters)
   at Sitecore.Pipelines.CorePipeline.Run(PipelineArgs args)
   at Sitecore.Pipelines.DefaultCorePipelineManager.Run(String pipelineName, PipelineArgs args, String pipelineDomain, Boolean failIfNotExists)
   at Sitecore.Pipelines.DefaultCorePipelineManager.Run(String pipelineName, PipelineArgs args, String pipelineDomain)
   at Sitecore.LayoutService.ItemRendering.Pipelines.GetLayoutServiceContext.GetLayoutServiceContextPipeline.GetData(GetLayoutServiceContextArgs args)
   at Sitecore.LayoutService.ItemRendering.LayoutService.Render(Item item, IRenderingConfiguration renderingConfiguration, RenderOptions renderOptions)
   at Sitecore.LayoutService.Mvc.Controllers.LayoutServiceController.RenderItem(String configuration, String item)

What!? I was confused as this processor was working fine when not using the ref attribute, what is different? Like all good Sitecore developers, I instantly asked Google, and after reading through several search results, I decided that I needed to take the next step and crack open dotPeek.

I navigated to the type in question:

Sitecore.Pipelines.CorePipelineFactory.GetObjectNode

Looked at the code and stared in confusion at my screen.

private static XmlNode GetObjectNode(string objectName, XmlNode groupNode)
{
    XmlNode xmlNode = groupNode.SelectSingleNode("types/" + objectName);
    Error.Assert((xmlNode != null ? 1 : 0) != 0, "Could not find object node: " + objectName + " (group: " + CorePipelineFactory.GetGroupName(groupNode) + ")");
    return xmlNode;
}

What is this types node where Sitecore is trying to select a child node with a name of my processor type? The other thought I had was "Now I will finally have to understand how the hell pipeline groups work." The second thought was more straightforward to understand that I have thought initially, but first, if you were not aware of pipeline groups before reading this post they are a way of namespacing processor definitions, there is a Sitecore StackExchange question on this if you want some more detail or use cases for the concept.

Looking back up the Stack Trace from the above exception I decided to open up Sitecore.LayoutService.ItemRendering.Pipelines.GetLayoutServiceContext.GetLayoutServiceContextPipeline.GetData(GetLayoutServiceContextArgs args)

public IDictionary<string, object> GetData(GetLayoutServiceContextArgs args)
{
    Assert.ArgumentNotNull((object) args, nameof (args));
       this.PipelineManager.Run("getLayoutServiceContext", (PipelineArgs) args, this.Configuration.PipelineGroupName);
    return args.ContextData;
}

The above code kicks off the getLayoutServiceContext pipeline passing the group layoutService which was no surprise as this was what the configuration hinted at. What happens next is not rocket science, as the only real difference is when the XPath queries are constructed to select the pipeline configuration they are scoped to a group that is passed in.

See "group[@groupName='" + pipelineGroup + "']/pipelines/" below.

public override CorePipeline GetPipeline(string pipelineName, string pipelineGroup)
{
    Assert.ArgumentNotNullOrEmpty(pipelineName, nameof (pipelineName));
    Assert.ArgumentNotNull((object) pipelineGroup, nameof (pipelineGroup));
    string str = pipelineGroup + "\\" + pipelineName;
    CorePipeline pipeline = (CorePipeline) this.pipelines[(object) str];
    if (pipeline == null)
    {
        pipeline = this.CreatePipeline(pipelineName, pipelineGroup);
        if (pipeline != null)
        {
            lock (this.pipelines.SyncRoot)
            {
                if (this.pipelines[(object) str] == null)
                    this.pipelines[(object) str] = (object) pipeline;
            }
        }
    }
    return pipeline;
}

private CorePipeline CreatePipeline(string pipelineName, string pipelineGroup)
{
    XmlNode pipelineNode = this.GetPipelineNode(pipelineName, pipelineGroup);
    if (pipelineNode == null)
      return (CorePipeline) null;
    CorePipeline corePipeline = new CorePipeline(pipelineName);
    corePipeline.Initialize(pipelineNode);
    return corePipeline;
}

private XmlNode GetPipelineNode(string pipelineName, string pipelineGroup)
{
    string str = string.Empty;
    if (pipelineGroup.Length > 0)
    str = "group[@groupName='" + pipelineGroup + "']/pipelines/";
    return this.factory.GetConfigNode("pipelines/" + str + pipelineName);
}

One thing that I wanted to point out is that the code names the concept pipelineDomain and pipelineGroup, whereas the config only calls it a group. Not sure why and there could be a good reason, but it does feel misleading at face value, but that being said it does not appear that the group/domain is exposed on the CorePipeline object so only confusion for the Sitecore Devs :).

Back to the issue at hand why won't Sitecore use my factory? Well, it turns out that it is looking for the definition of how to construct the processor type as a child of types in the pipeline group. Here is my final configuration.

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
    <sitecore>
        <pipelines>
            <group groupName="layoutService">
                <types>
                    <ExampleContextExtension ref="SitecoreJss.Examples.ExampleContextExtension, SitecoreJss.Examples" factory="FactoryContainerAdapter">
                        <AllowedConfigurations hint="list">
                            <!-- Unless you change the Layout Service config used by your JSS app, this should always be present. -->
                            <jss>jss</jss>
                        </AllowedConfigurations>
                        <Applications hint="list">
                            <!-- Restrict the JSS apps for which this processor will execute. -->
                            <reactApp>JssReactWeb</reactApp>
                        </Applications>
                        <Routes hint="list">
                            <!-- Restrict the route items for which this processor will execute. IDs, item paths, and wildcard item paths are allowed. -->
                            <services>/sitecore/content/JssReactWeb/Home/Services*</services>
                            <portfolio>{BAD2C001-1746-4312-8422-B28806A1191E}</portfolio>
                        </Routes>
                    </ExampleContextExtension>
                </types>
                <pipelines>
                    <getLayoutServiceContext>
                        <processor ref="ExampleContextExtension"/>
                    </getLayoutServiceContext>
                </pipelines>
            </group>
        </pipelines>
    </sitecore>
</configuration>

I was initially annoyed at this change as I wasted a big chunk of my own time working it out, but my stubbornness for not just settling for the Service Locator Anti-Pattern paid off in the form of this blog post and also exposure to what I think might be a better practice. Defining how to resolve a processor in this manner makes it easier to reuse without repeating yourself again and again, with this example, the same processor could be used many times with just different configuration.