Sharing Controller Actions with ControllerExtenders

Why?

If you wanted to be able to share Actions between controllers in .Net there is currently no official way to do it and that is probably because it might not be something that people have thought about doing very often before. Though once I started thinking about it I realized that this concept could be used for a variety of different purposes and that other people might potentially think of whole new ways to apply this concept. Here’s a couple use cases:

  • Separating logic out of large controllers
  • Ability to distribute Controllers in DLLs whose Actions that could then be consumed by other people’s Controllers
  • Support for a ‘Multiple inheritance’ class structure

Again I think that there is some potential here for people to run with this concept in their own ways. Its probably not a concept that everyone will want/need to use, but you should know that it definitely can be done, and here’s how…

ControllerExtender

In order to extend your Controller with actions from another Controller, you’ll need to register your extension with a new class called ControllerExtender. There’s a couple of ways to go about doing this, the nicest way is to use attributes on your Controller:

[ExtendedBy(typeof(TestExtenderController))]
public class ContentEditorController : Controller
{....}

Otherwise there’s a few overloads on the ControllerExtender class to directly do this:

//Where 'this' is the controller you are extending, generally called in the 
//constructor of your controller
ControllerExtender.RegisterExtender(this, typeof (TestExtenderController));

//Where 'this' is the controller you are extending, generally called in the 
//constructor of your controller
ControllerExtender.RegisterExtender<TestExtenderController>(this);

//This registration could be created in your global.asax 
ControllerExtender.RegisterExtender<ContentEditorController, TestExtenderController>();

One important thing to note is that the ControllerExtender uses the DependencyResolver to create an instance of the extending controller class so you’ll need to ensure that your controllers are registered properly in IoC.

Illegal Extenders

The ControllerExtender will not let you extend a controller by it’s same type of it’s same sub type. Also, nested extenders do not work (though the code could be modified to support this) therefore you cannot have Controller ‘A’ be extended by Controller ‘B’ which is extended by Controller ‘C’. In this scenario when rendering actions on Controller ‘A’, only actions on Controller ‘A’ and ‘B’ will be resolved. When rendering actions on Controller ‘B’ only actions on Controller ‘B’ and ‘C’ will be resolved.

Multiple Extenders

You can register multiple extenders for one Controller but because multiple extenders may have the same Action name/signature, only the first one that is found that is a match is used. It is therefor up to the developer to make sure they are aware of this.

Custom IActionInvoker

In order to facilitate the sharing of Actions a custom IActionInvoker of type ControllerExtenderActionInvoker needs to be registered on your controller. This is easy to do in the constructor of your controller:

public MyController()
{
    this.ActionInvoker = new ControllerExtenderActionInvoker();
}

Source Code

The source code for all of this is pretty straight forward and boils down to 3 classes:

  • The static ControllerExtender class
  • The custom ControllerExtenderActionInvoker
  • The ExtendedByAttribute

ControllerExtenderActionInvoker class

This class is responsible for finding and invoking the correct Action on a controller which takes into account any of the extenders registered for the currently executing Controller.

The most up to date code can be found in the Umbraco 5 source code HERE.

ControllerExtender class

This class is responsible for maintaining the extender registrations, it allows for creating registrations and returning registrations:

public static class ControllerExtender
{

    /// <summary>
    /// Internal ConcurrentDictionary to store all registrations
    /// </summary>
    private static readonly ConcurrentDictionary<Tuple<Type, Type>, Func<ControllerBase>> 
        Registrations
            = new ConcurrentDictionary<Tuple<Type, Type>, Func<ControllerBase>>();

    /// <summary>
    /// Registers the extender.
    /// </summary>
    /// <typeparam name="TController">The type of the controller.</typeparam>
    /// <typeparam name="TExtender">The type of the extender.</typeparam>
    public static void RegisterExtender<TController, TExtender>()
        where TController : ControllerBase
        where TExtender : ControllerBase
    {
        var t = new Tuple<Type, Type>(typeof(TController), typeof(TExtender));
        if (Registrations.ContainsKey(t))
            return;

        if (typeof(TExtender).IsAssignableFrom(typeof(TController)))
        {
            throw new InvalidOperationException
                ("Cannot extend a controller by it's same type");
        }

        Registrations.TryAdd(t, () => DependencyResolver.Current.GetService<TExtender>());
    }

    /// <summary>
    /// Registers the extender.
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="controllerToExtend">The controller to extend.</param>
    public static void RegisterExtender<T>(
        ControllerBase controllerToExtend)
        where T : ControllerBase
    {
        RegisterExtender(controllerToExtend, () => DependencyResolver.Current.GetService<T>());
    }

    /// <summary>
    /// Registers the extender.
    /// </summary>
    /// <param name="controllerToExtend">The controller to extend.</param>
    /// <param name="controllerExtender">The controller extender.</param>
    public static void RegisterExtender(
        ControllerBase controllerToExtend, 
        Type controllerExtender)
    {
        var t = new Tuple<Type, Type>(controllerToExtend.GetType(), controllerExtender);
        if (Registrations.ContainsKey(t))
            return;

        if (controllerExtender.IsAssignableFrom(controllerToExtend.GetType()))
        {
            throw new InvalidOperationException
                ("Cannot extend a controller by it's same type");
        }

        Registrations.TryAdd(t, 
            () => DependencyResolver.Current.GetService(controllerExtender) as ControllerBase);
    }

    /// <summary>
    /// Registers the extender.
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="controllerToExtend">The controller to extend.</param>
    /// <param name="extender">The extender.</param>
    public static void RegisterExtender<T>(
        ControllerBase controllerToExtend, 
        Expression<Func<T>> extender)
        where T : ControllerBase
    {
        var extenderType = typeof(T);
        var t = new Tuple<Type, Type>(controllerToExtend.GetType(), extenderType);
        if (Registrations.ContainsKey(t))
            return;

        if (extender.GetType().IsAssignableFrom(controllerToExtend.GetType()))
        {
            throw new InvalidOperationException
                ("Cannot extend a controller by it's same type");
        }

        Registrations.TryAdd(t, extender.Compile());
    }

    /// <summary>
    /// Returns all registrations as a readonly collection
    /// </summary>
    /// <returns></returns>
    public static IEnumerable<KeyValuePair<Tuple<Type, Type>, Func<ControllerBase>>> 
        GetRegistrations()
    {
        return Registrations;
    }

}

ExtendedByAttribute class

This Attribute is used by the ControllerExtenderActionInvoker to created Extender registrations dynamically.

[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
public class ExtendedByAttribute : Attribute
{
    public Type ControllerExtenderType { get; private set; }

    public ExtendedByAttribute(Type controllerExtenderType)
    {
        if (!typeof(ControllerBase).IsAssignableFrom(controllerExtenderType))
        {
            throw new ArgumentException
                ("controllerExtenderType must be of type Controller");
        }
        ControllerExtenderType = controllerExtenderType;       
    }
}

Author

Administrator (1)

comments powered by Disqus