Implementing generic controllers in ASP.NET MVC and WebAPI – Part 1: Attribute-based routing with inheritance

Once in a while Mountain Warehouse runs competition campaigns like Britain’s Best Post-walk Pint or Lights, Camera, Backpack. Most of the time the principle is the same – users submit entries on the website (usually a picture with some description and basic info about themselves), then people vote and a winner is selected (sometimes it takes several rounds of voting). After building several independent websites that were very similar in functionality (but yet a bit different), we decided it would be worth creating a reusable framework for all future competition websites.

The look of each competition would be quite different, so each one has to have its own set of views. But the controllers code can and should be reused. In the first part of this post series I will talk about MVC controllers, while the second one will be about Web API part. 

Essentially reusing common functionality in the Competition framework goes down to creating a base controller, that would take ICompetetionService in the constructor. Each one of the derived controllers (each competition would have its own controller) would pass a specific implementation of the service. Pretty seamless, apart from few tweaks that had to be done to the MVC framework configuration.

Inherited route attributes in MVC controllers

The tricky bit in inheriting controller was to inherit the routing. Using attribute routing, how can we make sure the derived controllers use action routes from the base controller with custom route prefixes? In order to accomplish that you need to do two things – one is pretty straight forward, another – not so much.

First, we need to override DefaultDirectRouteProvider. The reason for this is that, if you look inside the implementation of this class, you see that in the method that gets route attributes (GetActionRouteFactories) GetCustomAttributes function of actionDescriptor is called with inherit parameter set to false. So all we need to do is to override this method, like this:

public class InheritedDirectRouteProvider : DefaultDirectRouteProvider
{
    protected override IReadOnlyList GetActionRouteFactories(ActionDescriptor actionDescriptor)
    {
        return actionDescriptor.GetCustomAttributes(typeof(IDirectRouteFactory), true).Cast().ToArray();
    }
}

Now you need to register the extended route provider in the RouteConfig:

public class RouteConfig
{
    public static void RegisterRoutes(RouteCollection routes)
    {
        routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
        routes.MapMvcAttributeRoutes(new InheritedDirectRouteProvider());

        routes.MapRoute(
            name: "Default",
            url: "{controller}/{action}/{id}",
            defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
        );
    }
}

You think it would work, but here is the tricky bit, which I discovered looking at the decompiled code as well. Apparently the standard RouteAttribute from System.Web.Mvc has Inherited parameter set to false:

///
Place on a controller or action to expose it directly via a route.             When placed on a controller, it applies to actions that do not have any System.Web.Mvc.RouteAttribute’s on them.

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = false)]
public sealed class RouteAttribute : Attribute, IDirectRouteFactory, IRouteInfoProvider
{
    …
}

So I created my own Route attribute, with the only difference that the Inherited parameter set to true. Here is how it looks like:

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)]
public class InheritedRouteAttribute : Attribute, IDirectRouteFactory, IRouteInfoProvider
{
    public string Name { get; set; }
    public int Order { get; set; }
    public string Template { get; private set; }

    public InheritedRouteAttribute() : this(string.Empty)
    {
    }

    public InheritedRouteAttribute(string template)
    {
        Template = template;
    }

    RouteEntry IDirectRouteFactory.CreateRoute(DirectRouteFactoryContext context)
    {
        var builder = context.CreateBuilder(this.Template);
        builder.Name = this.Name;
        builder.Order = this.Order;
        return builder.Build();
    }
}

Next step is applying the InheritedRoute attribute to actions in the base controller, like this:

public abstract class BaseCompetitionController : Controller
{
    private readonly ICompetitionService _competitionService;

    protected BaseCompetitionController(ICompetitionService competitionService)
    {
        _competitionService = competitionService;
    }

    [InheritedRoute("")]
    public virtual ActionResult Index()
    {
        var viewModel = _competitionService.GetLandingPageViewModel();
        return View(viewModel);
    }

    [InheritedRoute("enter")]
    public virtual ActionResult Enter()
    {
        var viewModel = _competitionService.GetEnterPageViewModel();
        return View(viewModel);
    }
}

And add RoutePrefix to derived controllers:

[RoutePrefix("gapyear")]
public class CameraController : BaseCompetitionController
{
    public CameraController(ICameraCompetitionService competitionService) : base(competitionService)
    {
    }

    [Route("home")]
    public virtual ActionResult Index()
    {
        return base.Index();
    }
}

You can also override the default base controller routes just by setting Route attribute in the child controller action, see Index action in CameraController above.

In summary, two things you need to do to make attribute routing inheritable are:

  • Customise DefaultDirectRouteProvider, overriding GetActionRouteFactories method.
  • Create your own InheritedRouteAttribute that would have Inherited attribute set to true.

In Part 2 I cover how to make reusable ApiControllers.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s