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:
public class RouteConvention : IApplicationModelConvention
{
private readonly AttributeRouteModel _centralPrefix;
public RouteConvention(IRouteTemplateProvider routeTemplateProvider)
{
_centralPrefix = new AttributeRouteModel(routeTemplateProvider);
}
public void Apply(ApplicationModel application)
{
foreach (var controller in application.Controllers)
{
var matchedSelectors = controller.Selectors.Where(x => x.AttributeRouteModel != null).ToList();
if (matchedSelectors.Any())
{
foreach (var selectorModel in matchedSelectors)
{
selectorModel.AttributeRouteModel = AttributeRouteModel.CombineAttributeRouteModel(_centralPrefix,
selectorModel.AttributeRouteModel);
}
}
var unmatchedSelectors = controller.Selectors.Where(x => x.AttributeRouteModel == null).ToList();
if (unmatchedSelectors.Any())
{
foreach (var selectorModel in unmatchedSelectors)
{
selectorModel.AttributeRouteModel = _centralPrefix;
}
}
}
}
}
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.
public static class MvcOptionsExtensions
{
public static void UseCentralRoutePrefix(this MvcOptions opts, IRouteTemplateProvider routeAttribute)
{
opts.Conventions.Insert(0, new RouteConvention(routeAttribute));
}
}
So now your Startup may look like this:
public class Startup
{
public Startup(IHostingEnvironment env)
{}
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc(opt =>
{
opt.UseCentralRoutePrefix(new RouteAttribute("api/v{version}"));
});
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
loggerFactory.AddDebug();
app.UseMvc();
}
}
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:
public class ItemController : Controller
{
// overall route: /api/v{version}/item/{id}
[Route("item/{id}")]
public string GetById(int id, int version)
{
//do stuff with version and id
return $"item: {id}, version: {version}";
}
}
// gets merged into the global route prefix
[Route("other")]
public class OtherController : Controller
{
// overall route: /api/v{version}/other/resource/{id}
[Route("resource/{id}")]
public string GetById(int id, int version)
{
//do stuff with version and id
return $"other resource: {id}, version: {version}";
}
}
All the code for this post can be found on Github.