Updated for ASP.NET Core MVC 1.0.0
This article was originally written using code snippets for ASP.NET RC1. RC2 and 1.0.0 introduced some breaking changes for this code. To view the updated code you can navigate to the end of this article.
Routing has changed a lot in ASP.NET MVC 6. At first glance it looks pretty much the same as previous versions, using attributes like [Route] and [HttpPost] for defining routes declaratively within a controller. But under the hood it's a complete new implementation that takes care of routing. In this post I'll explain how I recently implemented versioning for an ASP.NET 5 MVC6 (rc1) application.
First off: what do we want? Versioning, yes, but using which strategy? There are different ways you can implement versioning for an application. Troy Hunt has an excellent article on this subject. Basically there are 3 ways:
- URL: the version of the API operation is specified within the URL, like http://yourdomain.com/api/v3/customer/3
- Custom request header: the version of the API operation is specified in a custom HTTP request header, like Api-version: 3
- Accept header: the version of the API operation is specified in the Accept-header, like Accept: application/json; api-version-3
In my previous blog post I talked about most REST APIs not being truly resource based. Not having to represent an entity (semantically speaking), I went with what I thought would be the easiest strategy to implement and consume: URL-versioning.
The routing model in MVC 6
Versioning URLs requires knowledge of routing in MVC 6. Sadly there's not much official documentation on this subject at this moment of writing. Currently all we have an example (the ASP.NET 5 template of a Web Application), the source code (which changes regularly) and the community. Luckily that's enough :-)!
Stephen Walter wrote a nice article about what's new in Routing in MVC 6 compared with previous versions. Next up is Filip Woj, who shows how you can localize your routes using a custom IApplicationModelConvention (this is the new extensibility model in ASP.NET).
Having read those articles gave me enough background to start working on versioned routes.
The versioning model
Versioning is all about having stable contracts. At some time in the future a new version of a client is deployed, consuming a 'snapshot' of the API known at that precise moment. This is version N, and we want to make sure all clients consuming version N keep working until we explicitly say it's not supported any more. Operations may come and go between versions, and some may never change.
To avoid code duplication it would be nice to specify for what versions a specific operation is available. Creating a new version of the API would then simple be a matter of incrementing a single value at startup time, the MaxApiVersion. Based on supported versions specified on each operation and this MaxApiVersion, the application knows what URL routes to generate and set.
The implementation I wrote uses attribute routing. I suppose it would be possible to implement versioning using convention based routing, but I like having my routes and versions near my method signature. By extending the RouteAttribute class, the existing behavior like Route Constraints are kept intact.
Now we can replace the RouteAttribute with VersionedRouteAttributes where we want our operations to be versioned. Let's continue to work with the example as shown in the figure before.
The implementation of Create_1 could of course set missing values to a default and delegate the request to Create_2. AutoMapper would be great to take care of those mappings. Note the use of the [version]-token inside the controller route. Later on this token will be used to fill in the version number.
Based on the version metadata we can start generating the routes. The implementation supports both the RouteAttribute as the VersionedRouteAttribute. It's also possible the RouteAttribute is not defined at all, which means there is no path appended after the controller route. On to the code:
That was quite a bit of code.. basically it replaces existing actions on each controller with one or more new actions with a versioned route.
Finally the convention needs to be added in the ConfigureServices of the Startup.cs class, like this:
Now the application supports 3 versioned URLs, pointing to 2 implementations:
- http://yourdomain.com/api/v1/users/create, pointing to Create_1
- http://yourdomain.com/api/v2/users/create, pointing to Create_2
- http://yourdomain.com/api/v3/users/create, pointing to Create_2
Update for ASP.NET Core MVC 1.0.0
The Application Model of ASP.NET Core got an update. In order to register our versioning convention, we need to make some minor adjustments. Sadly the documentation still awaits its content. Luckily the code is open source, so I managed to find out what needs to be changed. You can skip to the code if you're not interested in the mechanics ;-)
Deep dive: the Application Model
My understanding is that the ApplicationModel represents your MVC application dynamically at runtime. Using IApplicationModelConventions you can apply custom behavior when the framework starts discovering actions. This discovering is done by the IApplicationModelProvider, which the framework provides a couple of implementations for. The most important for our context is called, straight to the point, DefaultApplicationModelProvider. This provider is responsible for building up the majority of the application model.
It's pretty interesting to see how the DefaultApplicationModelProvider builds up a model. Note for instance that the Order is set to -1000. The framework uses this property to call all known ApplicationModelProviders in that order, to ultimately build the ApplicationModel. Some other providers, like CorsApplicationModelProvider and AuthorizationApplicationModelProvider, have set their Order to -1000 + 10. They add their part of information to the application model later on.
It would be nice to map out some more details of MVC supported with schemas and such, but you came here for versioning. The DefaultApplicationModelProvider is showing us all we need to know.
A new conceptual model has been added: the SelectorModel. Basically the change is as follows: where we first copied each action for each possible route, we now have to keep that single action and add selectors for each route to its ActionModel. Easy as that!
Here's the tldr-where's-the-code: