API Versioning in ASP.NET Core made easy

Introduction

Let's start with the simple question: What is API Versioning? and Why do we need it?

What is API Versioning? API Versioning is the practice to transparently managing multiple versions of the same API. For example: You have an REST Endpoint which returns user-information. At the beginning of the project your user had only a name, but later on the user has also an address and you need to distinguish between first and last name. So every time you have a breaking API change API Versioning can come into play. That brings us to:

Why do we need it? Because it ensure stability, maintainability and reliability. Given the example from above. Maybe you have still users which are connected to the old version which does not distinguish between first and last name. Your client with the old version can't consume your new DTO. With a versioned API you can serve "both worlds". It increases stability and maintainability because every version of your API will be a separate controller / controller call. You can test version 1 and version 2 in isolation. They don't have to know eachother.

Setup

What do we need for API Versioning?

  • IDE of your choice with a running .NET 5 ASP.NET Core Web API Project. I work with JetBrains Rider
  • A tool for API testing like Postman or Fiddler. I personally prefer Postman for simple cases, but feel free to use whatever you want. We're just hitting some GET request against our endpoint.

At the end of the article you'll find the git repository to dive into the code directly.

Let's start by creating a new ASP.NET Core Web API Project. After we done this, we'll install the headliner of our show: Microsoft.AspNetCore.Mvc.Versioning to our web project. This package will provide all the functionality we need to do some versioning.

PM > Install-Package Microsoft.AspNetCore.Mvc.Versioning -Version 5.0.0

To enable versioning we have to inject the Versioning into our container so that the middleware can take care of it. Let's go into Startup.cs and register versioning in ConfigureServices and Configure like this:

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers();

    // This enables API Versioning. Later one we'll see what cool things we can do with it
    services.AddApiVersioning();
}

// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }

    app.UseApiVersioning();

Now let's create a new controller and a DTO which the controller returns:

using System;
using ApiVersioning.Dtos;
using Microsoft.AspNetCore.Mvc;

namespace ApiVersioning.Controllers
{
    [ApiController]
    public class UserController : ControllerBase
    {
        [Route("user")]
        [HttpGet]
        public ResponseDtoV1 GetUser()
        {
            return new ResponseDtoV1
            {
                UserId = Guid.NewGuid(),
                UserName = "Link",
            };
        }
    }
}

With the following DTO:

using System;

namespace ApiVersioning.Dtos
{
    public class ResponseDtoV1
    {
        public Guid UserId { get; set; }

        public string UserName { get; set; }
    }
}

I directly named my DTO V1. So you see that there might be coming a version 2 😉 Now we are good to go, hit compile and run and hit our endpoint with a GET Request: ApiRequired

Hmm, we're receiving an error: "An API version is required, but was not specified.". Well the reason is simple, the package expects a version in some form which we didn't provide (neither in the Controller nor the client when requesting). We have to explicitly tell the package to use some kind of default / fallback version if we didn't provide one.

So let's go back to our registration and let's configure all the magic. Our Startup.cs:

services.AddApiVersioning(options =>
{
     options.AssumeDefaultVersionWhenUnspecified = true;
});

This flags says basically: "Hey your Controller-Endpoint has no version so I assume it is exactly that version. Plus if your client doesn't send any version information I also assume it is the default."

Now let's hit compile again and do the same GET request: PostmanSuccess Yeah, we got an answer! Okay so now we know how to specify a default version. Later on I'll also show you how to explicitly set your default. But first we want to version our Controller

Versioning the Controller

Versioning our controller is super simple. We're just adding another attribute on our controller. And you have basically two ways of doing this

This:

[ApiController]
[ApiVersion("1.0")]
public class UserController : ControllerBase
{
    [Route("user")]
    [HttpGet]
    public ResponseDtoV1 GetUser()
    {
        return new ResponseDtoV1
        {
            UserId = Guid.NewGuid(),
            UserName = "Link",
        };
    }
}

Or:

[ApiController]
public class UserController : ControllerBase
{
    [Route("user")]
    [HttpGet]
    [ApiVersion("1.0")]
    public ResponseDtoV1 GetUser()
    {
        return new ResponseDtoV1
        {
            UserId = Guid.NewGuid(),
            UserName = "Link",
        };
    }
}

Either you put into on the controller class or on the method. It depends a bit on the way you work. I personally would recommend to put it onto the class level. Normally if you have a major break in your API changes are that you need have new dependencies and if you host all the responsibilities of all your versions into a single controller you have a big mess which is hard to test and maintain.

What I normally do is the following: Each controller version goes into its separate folder / namespace:

structure

So V1 has only the dependencies needed for V1 and V2 for V2 without knowing each other.

Back to our controller and the version. We annotated the controller with an API-Verison of 1.0. So let's compile and hit postman again:

PostmanVersion1

Now we see two things. First our call without any API Version still works thanks to the magic of AssumeDefaultVersionWhenUnspecified. Second our later call includes the version as part of the query parameter of the URL. If we use a version where we don't have an endpoint like https://localhost:5001/user?api-version=1.1 we get an 400 Bad Request: "The HTTP resource that matches the request URI 'https://localhost:5001/user' does not support the API version '1.1'."

Configure how the get the version

We saw that we can pass the API Version as query parameter in the URL. That is convenient but maybe not your way to work. Also this package got you covered. Remember the magical AddApiVersioning which gave us some options? Well, here we can configure how the middleware should detect the version. The responsible property is called options.ApiVersionReader. There are some predefined classes you can take. So let's have a look at them in a bit more detail, shall we?

Query String

Deduce the version from the query string. Like in our first example, but you also have the freedom the rename the parameter.

options.ApiVersionReader = new QueryStringApiVersionReader("api");

QueryParam

Url Segment

The version is part of the url. This is the most common way. To make this work we have to do two things. First: Declare the specific option for the API-Versioning middleware and second: on the controller level how the segment should look like.

First:

`options.ApiVersionReader = new UrlSegmentApiVersionReader();`

Second:

[ApiController]
[ApiVersion("1.0")]
public class UserController : ControllerBase
{
    [Route("v{version:apiVersion}/user")]
    [HttpGet]
    public ResponseDtoV1 GetUser()
    {
        return new ResponseDtoV1
        {
            UserId = Guid.NewGuid(),
            UserName = "Link",
        };
    }
}

We added a format-string into the route: [Route("v{version:apiVersion}/user")]. This will tell the middleware where the version string should be. Url Segment

Header Api Version

We also have the ability to declare the version as part of the request header:

//new HeaderApiVersionReader() is the same as new HeaderApiVersionReader("api-version")
options.ApiVersionReader = new HeaderApiVersionReader("version");

Header API Version

Media Type Api

The last one is a bit odd to say the least. We can specify the version as part of the media type in the HTTP header. I am not the biggest fan of that because we are kind of misusing this field.

options.ApiVersionReader = new MediaTypeApiVersionReader("api-version");

Accept

Combine

You also have the option to combine multiple mechanism to deduce the api version:

options.ApiVersionReader = ApiVersionReader.Combine(
    new HeaderApiVersionReader("api-version"),
    new QueryStringApiVersionReader("api-version")
);

A second version

Now we have everything in place and we can just create a new controller with version 2.0. Therefore we also need a new DTO which has a breaking change to version 1.0.

The DTO:

namespace ApiVersioning.Dtos
{
    public class ResponseDtoV2
    {
        public Guid UserId { get; set; }

        public string FirstName { get; set; }

        public string LastName { get; set; }
    }
}

And the controller:

using System;
using ApiVersioning.Dtos;
using Microsoft.AspNetCore.Mvc;

namespace ApiVersioning.Controllers.V2
{
    [ApiController]
    [ApiVersion("2.0")]
    public class UserController : ControllerBase
    {
        [Route("user")]
        [HttpGet]
        public ResponseDtoV2 GetUser()
        {
            return new ResponseDtoV2
            {
                UserId = Guid.NewGuid(),
                FirstName = "Warrior",
                LastName = "Link"
            };
        }
    }
}

Now let's assume we have the mechanism in place to derive the version from the query parameter.

Multiple versions

Setting the default version

Now we feel version 1.0 is a bit obsolete. So we want to first inform our users that the version is no longer supported and second we want to set our default version to 2.0.

options.ReportApiVersions = true;
options.DefaultApiVersion = new ApiVersion(2, 0);

Also let's declare version 1.0 as deprecated:

namespace ApiVersioning.Controllers.V1
{
    [ApiController]
    [ApiVersion("1.0", Deprecated = true)]
    public class UserController : ControllerBase
    {
        [Route("user")]
        [HttpGet]
        public ResponseDtoV1 GetUser()

So now if we don't provide any information we automatically get the response from version 2.0. But let's inspect the header from the response. We see two new properties: "api-supported-versions": "2.0" and "api-supported-versions": "1.0". So now our client was informed that 1.0 is obsolete.

Conclusion

We saw how we can add versioning to our API controller. Also how we can define te behavior how the middleware should retrieve the version from various ways.

Resources

  • Git-Repository: here
1
An error has occurred. This application may no longer respond until reloaded. Reload x