Two containers are better than one.

Sitecore is a fantastic CMS that does many things right, and from version 8.2 the list also included Dependency Injection. It's great to see the product embrace IoC, the framework's code is using it, training materials include the concept, and the Sitecore community is talking about it. I love IoC, and the more developers who understand the concept, the better, the only reservation I have is the use of Microsoft's Dependency Injection abstraction.
So why the reservation? I can use any third party contain I like right, that's the whole point of the new abstraction? Well like most things on this blog my concern is SOLID. Microsoft has defined a generic interface to interact with an underlying DI container, which could lead to a Dependency Inversion Principle violation. You however as an application developer have the power, and maybe the responsibility, not to take dependencies on framework abstractions and avoid this violation. If you are interested in more of my underlying thinking, consider reading .NET Junkie's blog post on the topic "What’s wrong with the ASP.NET Core DI abstraction?". The great thing about Sitecore from a developer’s point of view is that you can replace just about any of its functionality with something bespoke, this includes how it resolves dependencies. What I am going to illustrate here is something that may seem a bit counterintuitive, but there is a method to my madness, I am going to show you how to add a second DI Container, specifically the Simple Injector Container, and hopefully get you thinking about why this might be a good idea.
Even though Sitecore can handle requests for both MVC and WebForms, there are no considerations for how to use a second container for WebForms in this post. Sitecore supplies a custom controller factory to ASP.NET, this is performed in the Initialize
pipeline by the InitializeControllerFactory
processer, doing this allows Sitecore to have greater control over the resolution of controllers. If we want to achieve the same thing, there are three things we need to create.
Pipeline processor
public class InitialiseControllerFactory
{
public virtual void Process(PipelineArgs args)
{
var controllerBuilder = System.Web.Mvc.ControllerBuilder.Current;
var controllerFactory = new TwoContainersAreBetterThanOneControllerFactory(controllerBuilder.GetControllerFactory());
controllerBuilder.SetControllerFactory(controllerFactory);
}
}
Configuration
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
<sitecore>
<pipelines>
<initialize>
<processor type="TwoContainersAreBetterThanOne.CompositionRoot.InitialiseControllerFactory, TwoContainersAreBetterThanOne"
patch:instead="*[@type='Sitecore.Mvc.Pipelines.Loader.InitializeControllerFactory, Sitecore.Mvc']"/>
</initialize>
</pipelines>
</sitecore>
</configuration>
Controller factory
public sealed class TwoContainersAreBetterThanOneControllerFactory : SitecoreControllerFactory
{
private static readonly Container Container;
static TwoContainersAreBetterThanOneControllerFactory()
{
Container = Bootstrapper.Bootstrap();
}
public TwoContainersAreBetterThanOneControllerFactory(
IControllerFactory innerFactory) : base(innerFactory)
{
}
public override IController CreateController(RequestContext requestContext, string controllerName)
{
try
{
if (controllerName.EqualsText(SitecoreControllerName))
return CreateSitecoreController(requestContext, controllerName);
var controllerType = GetControllerType(requestContext, controllerName);
try
{
return Container.GetInstance(controllerType) as IController;
}
catch (ActivationException e)
{
Log.Info($"SimpleInjector could not resolve type, {controllerType.Name}", e);
}
return ResolveController(controllerType);
}
catch (Exception e)
{
if (MvcSettings.DetailedErrorOnMissingController)
throw CreateControllerCreationException(controllerName, e);
throw;
}
}
}
How to register your controllers in Simple Injector is not covered today but in the above example Bootstrapper.Bootstrap();
in the factory constructor returns a fully configured container. The second container is not a replacement and should only include registrations that relate to the application you are building, because of this our factory needs to be able to resolve controllers from both containers. It does this by:
- Checking if the requested controller is a Sitecore controller, using the default container for resolution of the requested type if it is.
- Controller resolution attempted from the Simple Injector container.
- Controller resolution falls back to the default container.
I have created a project that can be found here, as a working example. In this project, there is all the wiring that we have outlined above and a sample controller that greets you, aptly named HelloController. This controller takes a dependency on the logged in user, specifically the name property, but rather than taking a compile-time dependency on Sitecore.Context.User
I have opted to create a contract to access only the information that is needed. The dependency was inverted and is now a runtime one, and it gives us a few advantages. Creating small, focused adaptors like ILoggedOnUser
allows you to clearly understand when your code crosses system boundaries, in this argument the boundary is between my application code and Sitecore.
One of the effects of Dependency Inversion is the removal of compile time dependencies replacing them with runtime ones. Another advantage of small, focused adaptors other than conforming to the Interface Segregation Principle and improved testability, is because of the focused touch points between your application code and Sitecore you may experience less painful upgrades. If there was a major change to Sitecore's API, hopefully, no change should be required to application code, only an adaptor implementation rework. In my simple example I still take a reference on Sitecore, but the implementation of ILoggedOnUser
can be considered on the Sitecore side of the boundary and does not need to be in the same project.
There could be performance implications with my custom controller factory, though the increase you get in maintainability is a fair trade-off in my opinion, and when coupled with output caching I don’t see a net disadvantage, especially if you see developer time is where the real cost of building a website lives.