The Mystery of the SitecoreActionInvoker

This blog post contains information determined from my investigation of the SitecoreActionInvoker class in version 8 of the Sitecore ASP.NET web Content Management System (CMS) and Experience Platform (XP). The Sitecore action invoker wraps the default MVC action invoker with logic to throw an exception with details about missing actions.

An action invoker implements IActionInvoker, which defines an interface for invoking action methods. The ActionInvoker property exposes the action invoker of classes that inherit from the System.Web.Mvc.Controller. The default action invokers (ControllerActionInvoker and AsyncControllerActionInvoker) use the name of the action, the name of the method, and optional attributes such as HTTP method (GET, PUT, and so forth) to determine the name of the action method. It then invokes the method and the ActionResult that it returns, along with any action filters (including those that Sitecore uses to invoke the mvc.actionExecuting, mvc.actionExecuted, mvc.exception, mvc.resultExecuting, and mvc.resultExecuted pipelines).

Some time ago, I noticed that Sitecore includes a SitecoreActionInvoker class, so I decided to see what it does and how it works. Then I realized that Sitecore 8 actually includes multiple classes with ActionInvoker in their names:

  • Sitecore.Mvc.Controllers.ControllerRunnerActionInvoker: I cannot find any use of this class in code or config files; it may be obsolete.
  • Sitecore.Social.Client.Mvc.ActionInvoker: This class does not implement the IActionInvoker interface required for MVC ActionInvokers, inherit from a class that implements IActionInvoker, or implement an interface that extends IActionInvoker, and is therefore not an MVC action invoker.
  • Sitecore.Mvc.Controllers.SitecoreActionInvoker: This appears to be the only ActionInvoker class involved in Sitecore MVC.

SitecoreActionInvoker does not derive from the default System.Web.Mvc.ControllerActionInvoker, but implements IActionInvoker directly. The constructor for SitecoreActionInvoker accepts an instance of ActionInvoker and the name of a controller. The first parameter indicates the use of constructor injection to wrap the default ASP.NET MVC ActionInvoker with custom functionality. A constructor for a class that implements an interface accepting an instance of the same interface often indicates constructor injection, in this case to wrap the default action invoker. The constructor for SitecoreActionInvoker stores the default ActionInvoker in its InnerInvoker property.

The SitecoreActionInvoker only applies if the ActionInvoker property of controllers created by its CreateController() method is not null and the MvcSettings.DetailedErrorOnMissingAction setting in the Web.config file is true. In that case, Sitecore passes that ActionInvoker to the constructor for a new SitecoreActionInvoker, and then sets that ActionInvoker property of that controller to that new SitecoreActionInvoker, effectively wrapping its default action invoker. This appears to be the only use of the MvcSettings.DetailedErrorOnMissingAction setting.

The InvokeAction() method of SitecoreActionInvoker calls the InvokeAction() method of the action invoker exposed by its InnerInvoker property. If the InvokeAction() method for that inner action invoker returns false, the SitecoreActionInvoker throws an InvalidOperationException with details about the controller and action. The stack trace does not show what happens inside the InvokeAction() method of the inner ControllerActionInvoker, but only where Sitecore intercepted its result.

Under the default configuration, where the mode attribute of the /configuration/system.web/customErrors element is RemoteOnly and the MvcSettings.DetailedErrorOnMissingAction is true, an HTTP request for an action that does not exist results in exception details in the response such as the following:

[InvalidOperationException: Could not invoke action method: nosuch. Controller name: SitecoreJohn. Controller type: SitecoreJohn.Controllers.SitecoreJohnController]
   Sitecore.Mvc.Controllers.SitecoreActionInvoker.InvokeAction(ControllerContext controllerContext, String actionName) +237
   Sitecore.Mvc.Controllers.SitecoreActionInvoker.InvokeAction(ControllerContext controllerContext, String actionName) +46
   System.Web.Mvc.<>c__DisplayClass22.<BeginExecuteCore>b__1e() +40
   System.Web.Mvc.Async.AsyncResultWrapper.<.cctor>b__0(IAsyncResult asyncResult, Action action) +11
   System.Web.Mvc.Controller.EndExecuteCore(IAsyncResult asyncResult) +53
   System.Web.Mvc.Async.WrappedAsyncVoid`1.CallEndDelegate(IAsyncResult asyncResult) +19
   System.Web.Mvc.MvcHandler.<BeginProcessRequest>b__5(IAsyncResult asyncResult, ProcessRequestState innerState) +51
   System.Web.Mvc.Async.WrappedAsyncVoid`1.CallEndDelegate(IAsyncResult asyncResult) +111
   Sitecore.Mvc.Routing.RouteHttpHandler.EndProcessRequest(IAsyncResult result) +69
   System.Web.CallHandlerExecutionStep.System.Web.HttpApplication.IExecutionStep.Execute() +606
   System.Web.HttpApplication.ExecuteStep(IExecutionStep step, Boolean& completedSynchronously) +288

If the mode attribute of the /configuration/system.web/customErrors element is On or if the Mvc.DetailedErrorOnMissingAction setting is false, the server responds with the ASP.NET 404 page.