API Versioning with MVC 6

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.

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 original article on my blog.

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.

api versioning

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.

Specifying versions

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.

public class VersionedRouteAttribute : RouteAttribute
{
    public VersionedRouteAttribute(string template, int minVersion) : base(template)
    {
        MinVersion = minVersion;
    }

    public VersionedRouteAttribute(string template) : this(template, 1)
    {
    }

    public VersionedRouteAttribute(string template, int minVersion, int maxVersion) : this(template, minVersion)
    {
        MaxVersion = maxVersion;
    }

    public int MinVersion { get; set; }
    public int? MaxVersion { get; set; }
}

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.

[Route("api/v[version]/[controller]")]
public class UsersController : Controller
{
	[HttpPost]
	[VersionedRoute("Create", 1, 1)]
	public IActionResult Create_1(MyApplication.InputModels.V1.CreateUserDto dto)
	{
		// todo actually create a new user.
		return Create_2(forwardCompatibleDto);
	}

	[HttpPost]
	[VersionedRoute("Create", 2)]
	public IActionResult Create_2(MyApplication.InputModels.V2.CreateUserDto dto)
	{
		// todo actually create a new user.
		return Ok();
	}
}

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.

Generating routes

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:

public class RouteVersioningApplicationModelConvention : IApplicationModelConvention
{
	private const string VersionToken = "[version]";
	private readonly int _maxVersionNumber;

	public RouteVersioningApplicationModelConvention(int maxVersionNumber)
	{
		_maxVersionNumber = maxVersionNumber;
	}

	public void Apply(ApplicationModel application)
	{
		foreach (var controller in application.Controllers)
		{
			var routeTemplate = string.Empty;

			var controllerRoute = controller.AttributeRoutes.SingleOrDefault();
			if (controllerRoute != null)
			{
				routeTemplate = string.Concat(routeTemplate, controllerRoute.Template);
			}

			controller.AttributeRoutes.Clear();
			VersionControllerActions(controller, routeTemplate);
		}
	}
	
	private void VersionControllerActions(ControllerModel controller, string routeTemplate)
	{
		var newActions = CreateVersionedActions(controller, routeTemplate);

		controller.Actions.Clear();
		foreach (var newAction in newActions)
		{
			controller.Actions.Add(newAction);
		}
	}

	private IEnumerable<ActionModel> CreateVersionedActions(ControllerModel controller, string routeTemplate)
	{
		var newActions = new List<ActionModel>();
		foreach (var action in controller.Actions)
		{
			// Empty route needs to be versioned as well.
			if (action.AttributeRouteModel == null)
			{
				action.AttributeRouteModel = new AttributeRouteModel();
			}

			var minVersion = 1;
			var maxVersion = _maxVersionNumber;

			var actionRouteAttribute = action.AttributeRouteModel.Attribute as VersionedRouteAttribute;
			if (actionRouteAttribute != null)
			{
				minVersion = actionRouteAttribute.MinVersion;

				if (actionRouteAttribute.MaxVersion.HasValue)
				{
					maxVersion = actionRouteAttribute.MaxVersion.Value;
				}
			}

			for (var version = minVersion; version <= maxVersion; version++)
			{
				var newAction = CreateVersionedAction(action, routeTemplate, version);
				newActions.Add(newAction);
			}
		}

		return newActions;
	}
	
	private static ActionModel CreateVersionedAction(ActionModel action, string routeTemplate, int version)
	{
		var newAction = new ActionModel(action);
		var versionedRoute = routeTemplate.Replace(VersionToken, version.ToString());

		newAction.AttributeRouteModel.Template =
			$"{versionedRoute}/{newAction.AttributeRouteModel.Template}";

		return newAction;
	}
}

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:

public void ConfigureServices(IServiceCollection services)
{
	services.AddMvc().AddMvcOptions(options =>
	{
		options.Conventions.Add(new RouteVersioningApplicationModelConvention(3));
	});
}

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

This article can also be found on my personal blog at patrickniezen.com

You may also like...