Controllerless Actions with ASP.NET MVC
There's recently been some discussion online about the concept of using 'controllerless' actions with the MVC pattern and I wanted to see how easy this would be to implement on top of ASP.NET MVC.
What is a Controllerless Action?
In a typical ASP.MVC project, a controller maps to a class and and action maps to a method on that class, eg:
public class CustomerController : Controller { private CustomerRepository customerRepository; public CustomerRepository(CustomerRepository customerRepository) { this.customerRepository = customerRepository; } public ActionResult Index() { return View(); } public ActionResult Edit(int id) { var customer = customerRepository.FindById(id); return View(customer); } }
In this example visiting the URL /Customer/Index would invoke the Index method on the CustomerController, while /Customer/Edit/5 would invoke the Edit method (with the id coming in as a parameter).
The idea behind a controllerless aciton is that each action is a separate class, so our Index action above would look like this:
namespace MyApp.Controllers.Home { public class Index { public ActionResult Execute() { return new ViewResult(); } } }
What are the benefits of controllerless actions?
In a small/simple project there probably isn't much benefit. However, in a large application you'll often find that controllers can become quite large and complex. One of the biggest problems is that different actions in the same controller class require different dependencies. For example, check out this constructor from the OrderController in SutekiShop:
public OrderController( IRepository<Order> orderRepository, IRepository<Country> countryRepository, IRepository<CardType> cardTypeRepository, IEncryptionService encryptionService, IPostageService postageService, IUserService userService, IOrderSearchService searchService, IRepository<OrderStatus> statusRepository) { ... }
Not all of the actions in the OrderController make use of all of these dependencies. For example, the OrderSearchService is only used by the Index action. In this situation it would make sense to split the controller up into a separate class for each action. This way, each action has less baggage to carry around - it only takes dependencies on those things it actually needs. As a side effect, this helps with testability as you can test each action without having to provide stubs for the dependencies that the action doesn't use.
ASP.NET MVC Implementation
Jeffrey Palermo has already posted an excellent example of using controllerless actions with ASP.NET MVC. In his example, you have to inherit from a class called ActionController and then create a method called Execute to handle the request. Because ActionController inherits from the existing System.Web.Mvc.Controller this allows you to make use of all the existing features in ASP.NET MVC.
However, I wanted to try something slightly different. My aim here is:
- To allow action classes to be POCO (ie, not inherit from a base class)
- To allow method invocation semantics to be configurable by using IoC container
- To allow action filters to be configured externally (rather than using attributes)
- To re-use as much of the ASP.NET MVC framework infrastructure as possible
I managed to succeed with most of these points, but there are some fairly major problems with this approach (see below). To start with, let's explore the end result:
Example Application
In this example I'll be using the Autofac IoC container for driving the application. Code for this is example is linked to from the bottom of the post.
The first thing we need to do is configure our IoC container and register our controller factory (we also have to make our MvcApplication class implement Autofac's IContainerProviderAccessor and set up the container disposal HTTP Module - see the sample code for the implementation):
protected void Application_Start() { RegisterRoutes(RouteTable.Routes); InitialiseContainer(); //...more configuration } private void InitialiseContainer() { var builder = new ContainerBuilder(); builder.RegisterModule(new ControllerlessActionsModule()); provider = new ContainerProvider(builder.Build()); ServiceLocator.SetLocatorProvider(() => provider.RequestContainer.Resolve<IServiceLocator>()); }
Note we register a ControllerlessActionsModule with our container builder. Autofac's modules allow you to group registrations together:
public class ControllerlessActionsModule : Module { INamingConventions namingConventions = new DefaultNamingConventions(); protected override void Load(ContainerBuilder builder) { builder.Register(c => new AutofacServiceLocator(c)).As<IServiceLocator>().ContainerScoped(); builder.Register(namingConventions).ExternallyOwned(); builder.Register(c => new DefaultActionMethodSelector()).As<IActionMethodSelector>(); var actionTypes = new ControllerActionLocator(namingConventions) .FindActionsFromAssemblyContaining<Index>() .Where(x => x.Namespace.StartsWith("Web.Controllers")); foreach(var action in actionTypes) { builder.Register(action.Type).FactoryScoped().Named(action.Name); } } }
Here we set up our registrations with the container. Note that we are providing an implementation of INamingConventions. This is used to construct keys for the action registrations. The DefaultNamingConventions class assumes that action classes will live in a Namespace of [Application].Controllers.[Controller name]. So an "Index" action class in the "Home" namespace would be registered as "home.index.action". We also register the DefaultActionMethodSelector (more on this below) and then use the ControllerActionLocator to find all the action classes in our assembly.
Back in our Global.asax, we also need to register the ControllerlessControllerFactory in Application_Start:
ControllerBuilder.Current.SetControllerFactory(new ControllerlessControllerFactory(() => provider.RequestContainer.Resolve<IServiceLocator>()));
The ControllerlessControllerFactory asks the container to resolve our action class and then returns it for execution. Unfortunately, this is where things start to become difficult. The first problem is that the MVC Framework expects all controllers to implement the IController interface (which contains 1 method - Execute). This is a problem because we want our action classes to be POCO and have some other class responsible for their execution. To make matters worse, most of the framework expects controllers to inherit from ControllerBase (which raises the question why bother having the IController interface if you're going to require the use of a base class?)
We can work around this problem by using a ControllerAdaptor which inherits from System.Web.Mvc.Controller and wraps our action class. The ControllerlessControllerFactory's CreateController method looks like this:
public IController CreateController(RequestContext requestContext, string controllerName) { string actionName = requestContext.RouteData.GetRequiredString("action"); string key = NamingConventions.BuildKeyFromControllerAndAction(controllerName, actionName); object actionInstance; try { actionInstance = serviceLocator().GetInstance(typeof(object), key); } catch (Exception ex) { throw new HttpException(404, "Controller not found", ex); } if (actionInstance == null) { throw new HttpException(404, "Controller not found"); } return new ControllerAdaptor(actionInstance, serviceLocator()); }
The ControllerAdaptor replaces the MVC framework's default ControllerActionInvoker with a ControllerlessActionInvoker. The ControllerlessActionInvoker calls into the implementation of IActionMethodSelector registered with our container to find the method on our action class to execute. The default behaviour is first to look for a method matching the current HTTP verb (Get or Post) or if one of these methods cannot be found then it will look for a method called Execute.
With all this wiered up, our Action class can look like this:
namespace Web.Controllers.Home { public class Index { public object Get(int? id) { return new HomeViewModel { Id = id }; } public object Post(string name) { return new ContentResult { Content = "Hello there, " + name }; } } }
The Get method will respond to HTTP GET requests to /Home/Index and the Post method will respond to POSTs. Note that while our Post method returns a type derived from ActionResult, the Get method does not. By default in the MVC framework, if you return an object from a controller action which does not derive ActionResult then it is converted to a string and rendered to the response stream. However, we can use an ActionFilter to apply a different convention so that if an object is returned that is *not* an ActionResult then a View will be rendered with that object as its ViewData.
Action Filter Configuration
For the most part the controllerless ActionFilter implementation re-uses the MVC Framework's default implementation. The only difference is how we declare the filters. By default, the framework expects ActionFilters to be declared as Attributes that decorate our controller classes and action methods. I am not particularly fond of this approach as it is not suitable for use with dependency injection (although there are some workarounds - see here and here) so I wanted to externalise the filter configuration.
The ControllerlessActions sample contains a FilterCollection class that the ControllerlessActionInvoker will use to obtain filters for the current action. This can be configured in our autofac module:
var filters = new FilterCollection(); builder.Register(filters).ExternallyOwned(); builder.RegisterTypesAssignableTo<IActionFilter>().FactoryScoped(); builder.RegisterTypesAssignableTo<IAuthorizationFilter>().FactoryScoped(); filters.Apply<DecorateModelWithViewResult>().Always();
First, we register a FilterCollection with the container and also lazily register all ActionFilters and AuthorizationFilters that are in the project. The final line says that for every action that is invoked, execute the DecorateModelWithViewResult filter. This filter looks to see if the result of an action is an object that derives from ActionResult. If not, it wraps the object in a ViewResult:
public class DecorateModelWithViewResult : IActionFilter { public void OnActionExecuting(ActionExecutingContext filterContext) {} public void OnActionExecuted(ActionExecutedContext filterContext) { var result = filterContext.Result as ModelResult; if(result != null) { filterContext.Controller.ViewData.Model = result.Model; filterContext.Result = new ViewResult(); } } }
We can also configure action filters to only apply to certain actions:
filters.Apply<AddMessageToViewData>().When(action => action.ActionInstance is Controllers.Home.Index);
...or alternatively...
filters.Apply<AddMessageToViewData>().ForTypesAssignableTo<Controllers.Home.Index>();
Limitations
Unfortunately, the MVC Framework is really designed to work with controller classes that inherit from ControllerBase. Although we can hack around this by using a ControllerAdaptor and a custom ActionInvoker the implementation is not very neat and actually ends up having to bypass some key optimizations in the framework. For example, the ControllerDescriptorCache and the ActionMethodDispatcher cache only work for the default ActionInvoker implementation, and they cannot be extended as they are marked as internal (ugh). This probably means that my solution will not perform optimally (although I haven't run any benchmarks).
I also ended up having to copy-and-paste several methods from the MVC framework source into my sample application because they were marked as private/internal.
In the end, I'd tend to suggest that if you want to use controllerless actions with the ASP.NET MVC framework then you should consider using something like Jeffrey's ActionController base class as the solution is much cleaner and simpler. Using POCO is probably a more sensible choice if you're using a more open framework such as FubuMVC
Edit: Seb pointed out that one advantage of using POCO classes for your actions is that it makes it a lot easier to re-use them across different frameworks (eg OpenRasta).
Where's the code?
All the code from this post is up on my svn repository at https://www.jeremyskinner.co.uk/svn/public/trunk/ControllerlessActions (username 'guest') on GitHub and can be downloaded here.