In this article, we’re going to talk about API versioning and explore the options we have in ASP.NET Core.
So, let’s start.
Why Versioning
As developers, we often add new features to our apps and modify current APIs as well. Versioning enables us to safely add new functionality without breaking changes. But not all changes to APIs are breaking changes.
So, what can we define as a “breaking change” in an API endpoint?
Generally, additive changes are not breaking changes:
- Adding new Endpoints
- New (optional) query string parameters
- Adding new properties to DTOs
Replacing or removing things in our API cause breaking changes:
- Changing the type of DTO property
- Removing a DTO property or endpoint
- Renaming a DTO property or endpoint
- Adding a required field on the request
That said, we have to face versioning requirements sooner or later since it is a way to maintain backward compatibility.
Required Packages for API Versioning
For Versioning requirements, we are going to use Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer
NuGet package. It allows us to easily implement versioning in our ASP.NET Core applications with a few configuration lines.
That said, let’s install it:
PM> Install-Package Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer
After the installation, let’s set up the main configuration for versioning:
builder.Services.AddApiVersioning(o => { o.AssumeDefaultVersionWhenUnspecified = true; o.DefaultApiVersion = new Microsoft.AspNetCore.Mvc.ApiVersion(1, 0); o.ReportApiVersions = true; o.ApiVersionReader = ApiVersionReader.Combine( new QueryStringApiVersionReader("api-version"), new HeaderApiVersionReader("X-Version"), new MediaTypeApiVersionReader("ver")); });
With the AssumeDefaultVersionWhenUnspecified
and DefaultApiVersion
properties, we are accepting version 1.0 if a client doesn’t specify the version of the API. Additionally, by populating the ReportApiVersions
property, we show actively supported API versions. It will add both api-supported-versions
and api-deprecated-versions
headers to our response.
Finally, because we are going to support different versioning schemes, with the ApiVersionReader
property, we combine different ways of reading the API version (from a query string, request header, and media type).
In addition to this configuration, we are going to modify our Program
class a bit more:
builder.Services.AddVersionedApiExplorer( options => { options.GroupNameFormat = "'v'VVV"; options.SubstituteApiVersionInUrl = true; });
The specified format code GroupNameFormat
will format the version as “‘v’major[.minor][-status]”. Furthermore, another property SubstituteApiVersionInUrl
is only necessary when versioning by the URI segment.
Now, we can add some test data to test different API versions:
public class Data { public static readonly string[] Summaries = new[] { "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" }; }
Our sample project will return different strings on different versions. API Version one is going to return strings starting with “B”; version two is going to return strings starting with “S”, and version three is going to return strings starting with “C”.
After everything is prepared, let’s check the versioning schemes.
Query String Parameter Versioning
The default versioning scheme is Query String Parameter Versioning. We’ve already set a name for the query string parameter (api-version) that we are going to use to send versioning information.
Now, let’s create a controller with an action to return strings starting with “B” to clients:
[ApiController] [Route("api/[controller]")] [ApiVersion("1.0")] public class StringListController : ControllerBase { [HttpGet()] public IEnumerable<string> Get() { return Data.Summaries.Where(x => x.StartsWith("B")); } }
We use the [ApiVersion("1.0")]
attribute to set the version of the controller.
To test this, let’s send version information as a query string parameter in a GET request:
We can see that all the strings starting with “B” are returned as a response.
Media/Header API Versioning
This type of versioning makes URIs stay clean because we only modify the Accept header values. In this case, the scheme preserves our URIs between versions.
That said, let’s create another controller and mark it as version 2.0:
[ApiController] [Route("api/[controller]")] [ApiVersion("2.0")] public class StringListController : Controller { [HttpGet()] public IEnumerable<string> Get() { return Data.Summaries.Where(x => x.StartsWith("S")); } }
Then, we can send versioning information by providing X-Version
in the request’s header (as we’ve configured with the HeaderApiVersionReader
class in our configuration):
Clearly, we see that version 2.0 is selected in the custom header, and strings started with “S” are returned.
In addition to sending version information in headers, we can do the same via media type header. In our configuration, we use MediaTypeApiVersionReader("ver")
to state that ver should be a version information holder.
So, let’s modify our request and provide the Accept
header :
We get the same result here.
Finally, since we now have two controllers, we can test what is going to happen if we send a request without version information:
The default version is selected and strings starting with “B” are returned to the client.
URI Versioning
URI versioning is the most common versioning scheme because the version information is easy to read right from the URI – so it is an advantage.
To see this in action, we are going to create another controller and set its version to 3.0:
[ApiController] [Route("api/v{version:apiVersion}/StringList")] [ApiVersion("3.0")] public class StringListController : Controller { [HttpGet()] public IEnumerable<string> Get() { return Data.Summaries.Where(x => x.StartsWith("C")); } }
Clearly, our action from the V3 controller returns strings starting with “C”.
Additionally, in the Route
attribute, we set a route substitution stating that the API version must be in the URI with the v{version:apiVErsion}
format.
That said, we can include the required version in our request:
https://localhost:7114/api/v3/stringlist
And our API returns all the strings starting with “C”:
[ "Chilly", "Cool" ]
Deprecating Previous Versions
As new features are added to the API and the old versions start to increase in numbers, supporting old API contracts introduces maintenance overhead. Along with that, we have to get rid of old versions.
API Versioning package allows us to flag APIs as deprecated. So this gives time to the client to prepare changes. Otherwise immediately deleting older APIs could give a bad taste to clients.
That said, all we have to do to set the deprecated version is to use an additional Deprecated
property in the ApiVersion
attribute:
[ApiController] [Route("api/[controller]")] [ApiVersion("1.0", Deprecated = true)] public class StringListController : ControllerBase { [HttpGet()] public IEnumerable<string> Get() { return Data.Summaries.Where(x => x.StartsWith("B")); } }
API version 1.0 is deprecated and the client will get this information from the header of api-deprecated-versions
from response.
Conclusion
APIs have to evolve and different versions will start to co-exist. So this co-existence leads to the practice of transparently managing changes to our API called Versioning. We need to add new APIs and gracefully deprecate old ones.
To sum up, in this article we’ve learned:
- What is versioning and how to configure it
- How to use different versioning schemas in our project
- The way to add deprecated versions