Global route prefixes with attribute routing in ASP.NET Web API

Β· 632 words Β· 3 minutes to read

As you may have learnt from some of the older posts, I am a big fan, and a big proponent of attribute routing in ASP.NET Web API.

One of the things that is missing out of the box in Web API’s implementation of attribute routing, is the ability to define global prefixes (i.e. a global “api” that would be prepended to every route) - that would allow you to avoid repeating the same part of the URL over and over again on different resources. However, using the available extensibility points, you can provide that functionality yourself.

Let’s have a look.

RoutePrefixAttribute πŸ”—

ASP.NET Web API allows you to provide a common route prefix for all the routes within a controller via RoutePrefixAttribute. This is obviously very convenient, but unfortunately that attribute, out of the box, cannot be used globally.

Here’s a typical use case per controller:

[RoutePrefix("api/items")]  
public class ItemsController : ApiController  
{  
[Route("")]  
public IEnumerable<Item> Get() { &#8230; }

[Route("{id:int}")]  
public Item Get(int id) { &#8230; }

[Route("")]  
public HttpResponseMessage Post(Item item) { &#8230; }  
}  

Of course in use cases like the one above, it would be nice to have to avoid that “api” repeated on every single controller.

Applying a route prefix globally πŸ”—

In order to apply route prefix globally, you will need to customize the way ASP.NET Web API creates routes from the route attributes. The way to do that, is to implement a custom IDirectRouteProvider, which can be used to feed custom routes into the Web API pipeline at application startup.

public interface IDirectRouteProvider  
{  
IReadOnlyList<RouteEntry> GetDirectRoutes(  
HttpControllerDescriptor controllerDescriptor,  
IReadOnlyList<HttpActionDescriptor> actionDescriptors,  
IInlineConstraintResolver constraintResolver);  
}  

That would typically mean a lot of work though, since you would need to manually deal with everything from A to Z - like manually processing controller and action descriptors. A much easier approach is to subclass the existing default implementation, DirectRouteProvider, and override just the method that deals with route prefixes (GetRoutePrefix). Normally, that method will only recognize route prefixes from the controller level, but we want it to also allow global prefixes.

DirectRouteProvider exposes all of its members as virtual - allowing you to tap into the route creation process at different stages i.e. when reading route prefixes, when reading controller route attributes or when reading action route attributes. This is shown in the next snippet:

public class CentralizedPrefixProvider : DefaultDirectRouteProvider  
{  
private readonly string _centralizedPrefix;

public CentralizedPrefixProvider(string centralizedPrefix)  
{  
_centralizedPrefix = centralizedPrefix;  
}

protected override string GetRoutePrefix(HttpControllerDescriptor controllerDescriptor)  
{  
var existingPrefix = base.GetRoutePrefix(controllerDescriptor);  
if (existingPrefix == null) return _centralizedPrefix;

return string.Format("{0}/{1}", _centralizedPrefix, existingPrefix);  
}  
}  

The CentralizedPrefixProvider shown above, takes a prefix that is globally prepended to every route. If a particular controller has it’s own route prefix (obtained via the base.GetRoutePrefix method invocation), then the centralized prefix is simply prepended to that one too.

Finally, the usage of this provider is as follows - instead of invoking the “regular” version of MapHttpAttributeRoutes method, you need to use the overload that takes in an instance of IDirectRouteProvider:

config.MapHttpAttributeRoutes(new CentralizedPrefixProvider("api"));  

In this example, all attribute routes would get “api” prepended to the route template.

In more interesting scenarios, you can also use parameters with CentralizedPrefixProvider - after all, we are just building up a regular route template here. For example, the following set up, defining a global version parameter as part of every route, is perfectly valid too:

config.MapHttpAttributeRoutes(new CentralizedPrefixProvider("api/v{version:int}"));  

Now in any action method signature, you can add an additional version integer parameter, even though you don’t have it in the route template beside the action - because it will be propagated down from the CentralizedPrefixProvider. For example:

[RoutePrefix("items")]  
public class ItemsController : ApiController  
{  
[Route("{id:int}")]  
public Item Get(int id, int version) { &#8230; }  
}  

And obviously - the “api” in the controller level RoutePrefixAttribute is no longer needed too.

About


Hi! I'm Filip W., a cloud architect from ZΓΌrich πŸ‡¨πŸ‡­. I like Toronto Maple Leafs πŸ‡¨πŸ‡¦, Rancid and quantum computing. Oh, and I love the Lowlands 🏴󠁧󠁒󠁳󠁣󠁴󠁿.

You can find me on Github and on Mastodon.

My Introduction to Quantum Computing with Q# and QDK book
Microsoft MVP