LibrarySites.Banner

Controller Constructor Injection with the Sitecore ASP.NET CMS

This blog post contains my findings from exploring how to implement constructor injection into controllers with the Sitecore ASP.NET web Content Management System (CMS) and Experience Platform (XP). With this technique, we use the ASP.NET controller factory to pass dependencies to constructors when creating controllers. We can use whatever techniques we like to resolve those dependencies.

For example, we may want to pass something from the new Sitecore.Abstractions assembly to the constructor for our controller, so that we can use that object from the controller. We can use a trivial controller to test. The constructor for this controller requires an ISettings object, which it stores in a private field. The Index() action method uses the TempFolderPath property from that ISettings instance.

namespace SitecoreJohn.Controllers
{
  using System.Web.Mvc;
 
  using Sitecore.Abstractions;
  using Sitecore.Diagnostics;
 
  public class MandatoryArgumentController : Controller
  {
    private ISettings _settings = null;
 
    public MandatoryArgumentController(ISettings settings)
    {
      Assert.ArgumentNotNull(settings, "settings");
      this._settings = settings;
    }
 
    public string Index()
    {
      Assert.IsNotNull(_settings, "_settings");
      return this._settings.TempFolderPath();
    }
  }
}

Controller constructor injection with Sitecore is not much different from controller constructor injection without Sitecore. We need override how the controller factory creates controllers. One differences from standalone ASP.NET is that we can use an <initialize> pipeline processor to attach our controller factory.

namespace SitecoreJohn.Pipelines.Loader
{
  using SitecoreJohn.Controllers;
 
  using System.Web.Mvc;
 
  public class InitializeControllerFactory
  {
    public void Process(Sitecore.Pipelines.PipelineArgs args)
    {
      ControllerBuilder.Current.SetControllerFactory(
        new SitecoreJohnControllerFactory(
          ControllerBuilder.Current.GetControllerFactory()));
    }
  }
}

Another difference is that in standalone ASP.NET, our custom controller factory would wrap DefaultControllerFactory. Sitecore 8 wraps DefaultControllerFactory with TagInjectionControllerFactory, which it further wraps with SitecoreControllerFactory. We have to think about where to insert our factory. TagInjectionControllerFactory and SitecoreControllerFactory do not override DefaultControllerFactory, but instead accept an IControllerFactory as a constructor parameter. That IControllerFactory instance can function something like a base class. For example, a factory can call methods in the factory passed to it, which can in turn call methods in its constructor argument, all the way down to DefaultControllerFactory.

Because SitecoreControllerFactory and TagInjectionControllerFactory use the factory passed to them to create any controllers not specific to their purposes, our logic should not affect theirs and their logic should not affect ours, and the order of wrapping should not matter. We can add our factory right above DefaultControllerFactory, letting SitecoreControllerFactory and TagInjectionControllerFactory wrap our factory. This seems to make sense as our factory needs to inherit from DefaultControllerFactory factory in order to call its GetControllerType() method. DefaultControllerFactory does not allow wrapping, and since our factory inherits from a complete implementation of IControllerFactory, we might forget to override every method in the interface just to wrap the inner factory. We need to treat the class passed to the constructor as if it were the base class. This could also be important because if other factories override methods that we do not override, and we wrap those other factories, our base class DefaultControllerFactory would override those overrides rather than wrapping them.

namespace SitecoreJohn.Controllers
{
  using System;
  using System.Web.Mvc;
  using System.Web.Routing;
  using System.Web.SessionState;
 
  using Sitecore.Abstractions;
  using Sitecore.Diagnostics;
  using Sitecore.Mvc.Helpers;
 
  public class SitecoreJohnControllerFactory : DefaultControllerFactory
  {
    IControllerFactory _innerFactory = null;
 
    public SitecoreJohnControllerFactory(
      IControllerFactory innerFactory)
    {
      Assert.ArgumentNotNull(innerFactory, "innerFactory");
      this._innerFactory = innerFactory;
    }
 
    public override IController CreateController(
      RequestContext requestContext,
      string controllerName)
    {
      Assert.ArgumentNotNull(requestContext, "requestContext");
      Assert.ArgumentNotNull(controllerName, "controllerName");
      Type controllerType = null;
 
      if (TypeHelper.LooksLikeTypeName(controllerName))
      {
        controllerType = TypeHelper.GetType(controllerName);
      }
 
      if (controllerType == null)
      {
        controllerType = this.GetControllerType(
          requestContext,
          controllerName);
      }
 
      if (controllerType != null)
      {
        // this might be a good place for an IoC
        if (controllerType == typeof(MandatoryArgumentController))
        {
          return TypeHelper.CreateObject<IController>
            (controllerType, new SettingsWrapper());
        }
      }
 
      Assert.IsNotNull(this._innerFactory, "_innerFactory");
      return this._innerFactory.CreateController(requestContext, controllerName);
    }
 
    public SessionStateBehavior GetControllerSessionBehavior(RequestContext requestContext, string controllerName)
    {
      Assert.IsNotNull(this._innerFactory, "_innerFactory");
      return this._innerFactory.GetControllerSessionBehavior(requestContext, controllerName);
    }
 
    public override void ReleaseController(IController controller)
    {
      Assert.IsNotNull(this._innerFactory, "_innerFactory");
      this._innerFactory.ReleaseController(controller);
    }
  }
}

We can use a Web.config include file such as the following to register our <initialize> pipeline processor:

<configuration xmlns:patch="https://www.sitecore.com/xmlconfig/">
  <sitecore>
    <pipelines>
      <initialize>
        <processor type="SitecoreJohn.Pipelines.Loader.InitializeControllerFactory, SitecoreJohn"
          patch:before="processor[@type='Sitecore.Apps.TagInjection.Loader.TagInjectionInitializer, Sitecore.Apps.TagInjection']" />
      </initialize>
    </pipelines>
  </sitecore>
</configuration>

Resources

  • _technically_ you can use WebActivatorEx and avoid the initialize pipeline.  And, for what it's worth, the ControllerRunner needs to be headed off at the pass as well (since this has a controller name check that jumps out of the IControllerFactory workflow).  Some caveats: * mvc.requestBegin pipeline needs to be altered to not use ControllerRunner. * mvc.renderRendering pipeline also needs to be altered to not use ControllerRunner. * The @Html.Sitecore().Controller() helper uses ControllerRunner as well.

  • Hi Brad,  The only thing that this post attempts to address is the actual instantiation of the controller class. I tested this in every controller context I could remember (map route to controller, Controller field in Layout section of item, and controller rendering). You might be thinking of other concerns, or I haven't hit the cases that you have. Are there some other APIs that invoke controllers? It seems that anything that uses ControllerBuilder.Current.GetControllerFactory() should work as long as nothing calls SetControllerFactory(). In any case I would like to hear more detail about the issues that you mention.  Thanks & regards,

  • After posting this I realized that, if the Mvc.DetailedErrorOnMissingAction setting is true, then the default controller factory wraps the action invoker for the controller with a SitecoreActionInvoker. This does not seem to be very significant, but you may wish to do the same after constructing your object with the CreateController method, as there doesn't seem to be any way to pass our controller back to the inner factory for this.  private void WrapActionInvoker(IController controller, string controllerName) {     if (MvcSettings.DetailedErrorOnMissingAction)     {         Controller controller2 = controller as Controller;         if (controller2 != null)         {             IActionInvoker actionInvoker = controller2.ActionInvoker;             if (actionInvoker != null)             {                 controller2.ActionInvoker = new SitecoreActionInvoker(actionInvoker, controllerName);             }         }     } }

  • Hi John, thanks for the great tip. But I found that if you specify a fully qualified name for your controller rendering, Sitecore (7.5) uses reflection to load the controller itself, so your solution don't work in this case.  Do you know how to bypass this? Thanks

  • @Marcos: You have a couple of choices: (1) Don't use a fully-qualified name, or (2) Do the work necessary to take "Sitecore.Mvc.Controllers.ControllerRunner, Sitecore.Mvc" out of the picture. It's that method that performs a call to TypeHelper.LooksLikeTypeName and uses reflection. (Which, IIRC, means modifying mvc.requestBegin, mvc.getRenderer among other things).

  • @Marcos: I use Autofac and I had the same issue. I created an AutofacControllerFactory similar to the one in this post and custom controller runner and renderer. Sitecore support now provide those custom classes as a support patch along with the config file and you only need to create the controller factory.