Isolated WebApi attribute routing

Attribute routing in ASP.Net WebApi is great and makes routing your controllers quite a bit more elegant than writing routes manually. However one problem I have with it is that it is either “on” or “off” at an application level.  There is no way for a library developer to tell ASP.Net to create routes based on attributes for specific controllers or assemblies without forcing the consumer of that library to enable Attribute Routing for the whole application. In many cases this might not matter, but if you are creating a package or library of that contains it’s own API routes, you probably don’t want to interfere with a developers’ normal application setup. There should be no reason why they need to be forced to turn on attribute routing in order for your product to work, and similarly they might not want your routes automatically enabled.

The good news is that this is possible. With a bit of code, you can route your own controllers with attribute routing and be able to turn them on or off without affecting the default application attribute routes. A full implementation of this has been created for the Umbraco RestApi project so I’ll reference that source in this post for the following code examples.

Show me the code

They key to getting this to work is: IDirectRouteProvider, IDirectRouteFactory

The first thing we need is a custom IDirectRouteFactory which is actually a custom attribute. I’ve called this CustomRouteAttribute  but you could call it whatever you want.

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true)]
public class CustomRouteAttribute : Attribute, IDirectRouteFactory

This custom attribute just wraps the default WebApi RouteAttribute’s IDirectRouteFactory implementation so we don’t have to re-write any code for that.

(see full implementation here)

Next we’ll create a custom IDirectRouteProvider:

/// <summary>
/// This is used to lookup our CustomRouteAttribute instead of the normal RouteAttribute so that 
/// we can use the CustomRouteAttribute instead of the RouteAttribute on our controlles so the normal
/// MapHttpAttributeRoutes method doesn't try to route our controllers - since the point of this is
/// to be able to map our controller routes with attribute routing explicitly without interfering
/// with default application routes.
/// </summary>
public class CustomRouteAttributeDirectRouteProvider : DefaultDirectRouteProvider
{
    private readonly bool _inherit;

    public CustomRouteAttributeDirectRouteProvider(bool inherit = false)
    {
        _inherit = inherit;
    }

    protected override IReadOnlyList<IDirectRouteFactory> GetActionRouteFactories(HttpActionDescriptor actionDescriptor)
    {
        return actionDescriptor.GetCustomAttributes<CustomRouteAttribute>(inherit: _inherit);
    }
}

So far this is all pretty straight forward so far but here’s where things start to get interesting. Because we only want to create routes for specific controllers, we need to use a custom IHttpControllerTypeResolver. However, since the HttpConfiguration instance only contains a single reference to the IHttpControllerTypeResolver we need to do some hacking. The route creation process for attribute routing happens during the HttpConfiguration initialization so we need to create an isolated instance of HttpConfiguration, set it up with the services we want to use, initialize it to create our custom routes and assign those custom routes back to the main application’s HttpConfiguration.

Up first, we create a custom IHttpControllerTypeResolver to only resolve the controller we’re looking for:

public class SpecificControllerTypeResolver : IHttpControllerTypeResolver
{
    private readonly IEnumerable<Type> _controllerTypes;

    public SpecificControllerTypeResolver(IEnumerable<Type> controllerTypes)
    {
        if (controllerTypes == null) throw new ArgumentNullException("controllerTypes");
        _controllerTypes = controllerTypes;
    }

    public ICollection<Type> GetControllerTypes(IAssembliesResolver assembliesResolver)
    {
        return _controllerTypes.ToList();
    }
}

Before we look at initializing a separate instance of HttpConfiguration, lets look at the code you’d use to enable all of this in your startup code:

//config = the main application HttpConfiguration instance
config.MapControllerAttributeRoutes(
    routeNamePrefix: "MyRoutes-",
    //Map these explicit controllers in the order they appear
    controllerTypes: new[]
    {                    
        typeof (MyProductController),
        typeof (MyStoreController)
    });

The above code will enable custom attribute routing for the 2 specific controllers. These controllers will be routed with attribute routing but instead of using the standard [Route] attribute, you’d use our custom [CustomRoute] attribute. The MapControllerAttributeRoutes extension method is where all of the magic happens, here’s what it does:

  • Iterates over each controller type
  • Creates an instance of HttpConfiguration
  • Sets it’s IHttpControllerTypeResolver instance to SpecificControllerTypeResolver for the current controller iteration (The reason an instance of HttpConfiguration is created for each controller is to ensure that the routes are created in the order of which they are specified in the above code snippet)
  • Initialize the HttpConfiguration instance to create the custom attribute routes
  • Copy these routes back to the main application’s HttpConfguration route table

You can see the full implementation of this extension method here which includes code comments and more details on what it’s doing.  The actual implementation of this method also allows for some additional parameters and callbacks so that each of these routes could be customized if required when they are created.

 

There is obviously a bit of code involved to achieve this and there could very well be a simpler way, however this implementation does work rather well and offers quite a lot of flexibility. I’d certainly be interested to hear if other developers have figured this out and what their solutions were.

Author

Shannon Thompson

I'm a Senior Software Engineer working full time at Microsoft. Previously, I was working at Umbraco HQ for about 10 years. I maintain several open source projects (many related to Umbraco) such as Articulate, Examine and Smidge, and I also have a commercial software offering called ExamineX. Welcome to my blog :)

comments powered by Disqus