ASP.NET MVC 3: Use re-mix library to add controller actions with mixins


Update 2012-01-15 I’ve written a follow-up post “ASP.NET MVC 3: Use re-mix library to add controller actions with mixins – Part 2” that shows how to integrate Fabian’s suggestions into the approach described in this post.

Update 2012-01-10 Fabian Schmied (re-mix developer) published a blog post “re-mix: Encapsulate and share ASP.NET MVC controller actions” which shows possible solutions to the problems I’ve listed in this post.

When developing ASP.NET MVC3 application one often needs to add similar actions to different controllers; for example controllers that enable the user to perform the basic CRUD (create, read, update, delete) operations typically contain action methods like Create(), Update() or Delete(). To avoid duplicate code a simple solution could be a common base controller. In complex applications this approach could have some limitations, since .NET does not support multi inheritance, e.g. each controller could have only one base controller.

This blog post shows a prototypically approach how to implement action methods that can be used in more than one controller with the help of the re-mix library which adds mixins support to .NET. Since the approach shown below is only a proof of concept, it has some limitations that are listed at the end of the post.

After downloading re-mix and adding references to the dll files contained in the folder net-3.5\bin\release one need a custom controller factory that uses remix’s ObjectFactory to create the controller objects:

public class RemixControllerFactory : IControllerFactory
{
  private IControllerFactory _defaultControllerFactory;

  public RemixControllerFactory()
  {
    _defaultControllerFactory = new DefaultControllerFactory();
  }

  public IController CreateController(RequestContext requestContext, string controllerName)
  {
    // Use the default ControllerFactory to get the type of the controller
    var controllerType = _defaultControllerFactory.CreateController(requestContext, controllerName).GetType();

    // Use remix's ObjectFactory to instantiate the (mixed) controller
    return (IController)ObjectFactory.Create(controllerType);
  }

  public SessionStateBehavior GetControllerSessionBehavior(RequestContext requestContext, string controllerName)
  {
    return SessionStateBehavior.Default;
  }

  public void ReleaseController(IController controller)
  {
  }
}

The custom RemixControllerFactory is set as the default controller factory in the Global.asax.as file:

protected void Application_Start()
{
  [...]
  ControllerBuilder.Current.SetControllerFactory(new RemixControllerFactory());
  [...]
}

In a next step we implement the interface that defines the action methods (Echo in this example) that will be used to extend the controllers:

public class IEchoControllerMixin
{
  ActionResult Echo(string id);
}

In a last step we implement the IEchoControllerMixin:

public class EchoControllerMixin : Mixin, IEchoControllerMixin
{
  public ActionResult Echo(string id)
  {
    var result = new ContentResult();
    var controllerName = Target.ControllerContext.RouteData.Values["controller"].ToString();
    result.Content = string.Format("re-mix added Echo action to controller '{0}'
You said: {1}", controllerName, !string.IsNullOrEmpty(id) ? id : "nothing"); return result; } [OverrideTarget] protected IActionInvoker CreateActionInvoker() { return new RemixControllerActionInvoker(typeof(IEchoControllerMixin)); } }

Due to security reasons ASP.NET MVC only allows to call action methods that are defined in a class that inherits System.Web.Mvc.ControllerBase. Since re-mix creates (automatically generated) proxy objects with their own inheritance structures, ASP.NET MVC does not find the Echo action by default. To bypass this limitation we implement a custom RemixControllerActionInvoker that allows calling actions that are defined in implementations of the IEchoControllerMixin interface. To do this we use reflection to call a non-public ASP.NET MVC functionality that creates an ActionDescriptor that skip the security check:

public class RemixControllerActionInvoker : ControllerActionInvoker
{
  private Type _remixType;

  public RemixControllerActionInvoker(Type remixType)
  {
    _remixType = remixType;
  }

  protected override ActionDescriptor FindAction(ControllerContext controllerContext, ControllerDescriptor controllerDescriptor, string actionName)
  {
    // Use default action descriptor
    ActionDescriptor actionDescriptor = controllerDescriptor.FindAction(controllerContext, actionName);
    if (actionDescriptor != null) return actionDescriptor;

    // Create action descriptor that accepts actions that are defined in the remix type
    var internalConstructor = typeof(ReflectedActionDescriptor).GetConstructor(
      BindingFlags.NonPublic | BindingFlags.Instance, null, new [] { typeof(MethodInfo), typeof(string), typeof(ControllerDescriptor), typeof(bool) }, null
    );
    return (ActionDescriptor)internalConstructor.Invoke(new object[] { _remixType.GetMethod(actionName), actionName, controllerDescriptor, false });
  }
}

After all the preparations we are finally able to “intermix” the Echo action to any controller:

[Uses(typeof(EchoControllerMixin))]
public class Controller1Controller : Controller
{
}
[Uses(typeof(EchoControllerMixin))]
public class Controller2Controller : Controller
{
}

To test the Echo action in both controllers, just open http://localhost:[…]/Controller1/Echo/Hello or http://localhost:[…]/Controller2/Echo/Hello.

Summarized mixins enable adding actions to controllers without the need of a common base controller; actions defined in mixins are completely decoupled from any inheritance structure and can be added to any controller.

As written in introduction, the approach shown above is only prototypical and has some limitations (which should be resolvable with some additional logic/code):

  1. Since the mixin overrides the CreateActionInvoker() of the controller that is “intermixed”, one could neither use more than one mixin in one controller nor use a custom CreateActionInvoker() method.
  2. In actions defined in mixins one does not have access to the protected ASP.NET MVC controller methods that create ActionResult objects like View(), PartialView(), Json(), Content(), …
  3. One needs to use reflection to call a protected ReflectedActionDescriptor constructor.
  4. The RemixControllerActionInvoker implementation does not consider the action parameter when creating the action descriptor. In result the action names defined in the mixins needs to be unique.

You can download the Visual Studio 2010 project containing all the source code here.

,

2 responses to “ASP.NET MVC 3: Use re-mix library to add controller actions with mixins”

Leave a Reply

Your email address will not be published. Required fields are marked *