Global route prefix in ASP.NET Core MVC (revisited)

A couple of months ago I blogged about adding a feature to ASP.NET Core MVC (or ASP.NET 5 at the time) that will allow you to set central route prefix(es) to your attribute routing mechanism.

That solution was written against beta8 version of ASP.NET Core and since now we are at RC2 – it doesn’t (surprise, surprise) work anymore.

Here is the updated version.

Global route prefixing in ASP.NET Core MVC overview

The original solution took advantage of the IApplicationModelConvention extensibility point – which allows you to inject custom conventions for the various configuration pieces of the MVC framework.

This still holds true – the extensibility point is the same in RC2. If you followed for example my work on typed routing for ASP.NET Core MVC (code is here on Github), that functionality uses IApplicationModelConvention to plug in the custom logic into MVC as well.

In the case of globally prefixing all your routes, we are going to use the same mechanism.

To recap: the ApplicationModel exposes a bunch of things about your MVC application – among them, all controllers that have been discovered. Then you can iterate through them, and access all discovered actions, action parameters and all useful framework components like that.

You can then plug your custom convention in against your MVC options object in the Startup class of your MVC application, and the convention will applied by the MVC runtime as your application is started.

So in this case we can leverage this, to inject a global route prefix into every controller.

Solution for ASP.NET Core MVC RC2

Again, the overall solution was already discussed in the original post.

The code, updated to RC2 is as follows:

What changed in RC2 is that the controllers and actions no longer expose a property AttributeRoutes on them. Instead, they have a collection of so called SelectorModels attached to them. This is an extra layer of flexibility, as it allows you to provide different action selection ways that can lead to this controller/action.

Attribute routing, along with action constraints (such as i.e. HTTP method constraint), is then exposed as a property of a selector model.

Usage

In order to use this, you need to insert this custom route convention into the MvcOptions at application startup.

So now your Startup may look like this:

In this case, api/v{version} will prefix every single route in your application. So you can now write controllers without any controller-level route attribute – or with a controller-level route attribute, and in both cases your global route prefix will be merged into them.

Below are two examples:

All the code for this post can be found on Github.

Be Sociable, Share!

  • John McKinzie

    I noticed that all your examples take an ID and thus have an additional path segment. You tested this against a collection endpoint? For example, GET on /api/v{version}/item, in order to return a list of item. I’ve tried to implement your solution on my service, but there appears to be a routing issue when I go to that type of endpoint. It ends up getting routed to the controller action that takes an ID, which fails b/c no ID was provided.

    • http://www.strathweb.com/ Filip W

      this is by design, if you define a parameter, such as “version” on a global prefix, then all your actions must take this parameter (even if it’s a throw away there).
      On the contrary, if you centralized prefix has no parameters, then you don’t need to add anything to your actions.

      • John McKinzie

        Thanks for the response. I figured out the issue. I had my prefix as “v{apiVersion}” instead of @”v{apiVersion}”. So based on what you were saying, apiVersion wasn’t being substituted, but rather is was becoming a route parameter.

  • Dave

    Thanks for the updated article! I was struggling with this after the RC2 update.

    With the updated version, I’m not sure that the Html/Tag Helpers are seeing the new Convention. In a View does not seem to produce the prefixed route:

    @Url.Action("MyActionView", "MyController")

    asp-controller="MyController" asp-action="MyActionView">

    Can you confirm this?

  • adnan

    Why not use a base class as route attributes can be inherited:

    [Route(“api/[controller]”)]
    public abstract class MyBaseController : Controller { … }

    public class ProductsController : MyBaseController
    {
    [HttpGet] // Matches ‘/api/Products’
    public IActionResult List() { … }

    [HttpPost(“{id}”)] // Matches ‘/api/Products/{id}’
    public IActionResult Edit(int id) { … }
    }