Routing to ASP.Net MVC ChildActions

in Software Engineering

As part of modern development practices to try and reuse code and functionality, we are constructing new website pages from components rather than building full pages each time.

The way we have chosen to do this in ASP.Net MVC is by creating our "components" with their own Controllers and Views so that all the functionality of a component is defined in one place and can be tested independently to the rest of the site.

We then make use of these components wherever we need them in our views using Html.RenderAction as so:

Html.RenderAction("Index", "ComponentController", argumentsObject);  

So, I create a new component, write all my tests, include the component in a view somewhere, build my application and navigate to that view and this appears:

No route in the route table matches the supplied values error page

What???

I've specified my Controller and Action as well as all the arguments I want it to use, so why is it in the routing tables looking for a route to this controller action?

It turns out that MVC requires Html.RenderAction (and similar ways of invoking Controller Actions from Views) to go via the routing table in order to set up context information around invoking a Controller Action. The details of exactly why this is the case would take this post quite off topic and likely require far more digging than is necessary.

So, how do we solve this problem?

A quick google suggests that this problem can be solved by making sure you haven't removed the default MVC route as this will solve your problem...

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

... and it will. However, there are sometimes good reasons why this has been removed. In our case it was Umbraco. Umbraco controls a fair amount of our routing and adds its routing control after all routes set up in the route config. This means we can't leave a default route lying around as it will intercept many of our valid Umbraco routes.

So, how else could we solve this?

Well since our problem is a lack of a route, we can just add one.

routes.MapRoute(  
  name: "MyComponentRoute",
  url: "ANY/UNIQUE/URL/YOU/WANT/",
  defaults: new { controller = "Component", action = "Index" }
);

And this will work fine. The problem is that when your entire website or application is made up of components you very quickly end up with a lot of routes that aren't actually responsible for any real externally accessible routes.

So, any other ideas?

Well if you happen to be working with MVC 5 or later then you are in luck. As part of MVC 5 the RouteAttribute was added. This attribute allows you to add routes directly on your controllers removing the need to pollute your route config file.

To use the RouteAttribute you just need to add the following in your route configuration:

routes.MapMvcAttributeRoutes();  

And then you can add routes as follows:

[Route("ANY/UNIQUE/URL/YOU/WANT/", Name = "MyComponentRoute", Order = 1)]

Problem solved... except that you still need to keep coming up with a unique url every time. What if someone accidentally copies the route for another component (nobody would ever copy and paste!). This isn't really ideal, especially since the url being chosen every time is never actually externally used.

So, how to solve that?

Realizing that I wanted a way to add an attribute for routing, but not require a route to be specified I dug into the code of RouteAttribute. I had hoped to extend RouteAttribute to just set the route randomly, but RouteAttribute has been made sealed. However, it does implement two interfaces IDirectRouteFactory and IRouteInfoProvider, which it turns out are used for hooking into the routing process and providing routes to the controllers decorated with RouteAttribute.

So I made my own attribute:

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = false)]
public class ChildActionRouteAttribute : Attribute, IDirectRouteFactory, IRouteInfoProvider  
{
  public ChildActionRouteAttribute()
  {
    var guidString = Guid.NewGuid().ToString();
    Template = guidString;
    Name = guidString;
  }

  RouteEntry IDirectRouteFactory.CreateRoute(DirectRouteFactoryContext context)
  {
    IDirectRouteBuilder builder = context.CreateBuilder(Template);
    return builder.Build();
  }

  public string Name { get; private set; }

  public string Template { get; private set; }
}

And voila... An attribute that creates a random route for a Controller or Controller Action that can be used for ChildActions.