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.
VIDEO: Web API Versioning in .NET.
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 Asp.Versioning.Mvc
and Asp.Versioning.Mvc.ApiExplorer
NuGet packages. They allow us to easily implement versioning in our ASP.NET Core applications with a few configuration lines.
That said, let’s install them:
PM> Install-Package Asp.Versioning.Mvc
PM> Install-Package Asp.Versioning.Mvc.ApiExplorer
After the installation, let’s set up the main configuration for versioning:
var apiVersioningBuilder = builder.Services.AddApiVersioning(o => { o.AssumeDefaultVersionWhenUnspecified = true; o.DefaultApiVersion = new 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:
apiVersioningBuilder.AddApiExplorer( 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 1.0 is going to return strings starting with “B”; version 2.0 is going to return strings starting with “S”, and version 3.0 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 starting 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")); } }
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. Due to this maintenance overhead, we need a way to get rid of old versions.
API Versioning package allows us to flag APIs as deprecated. This gives time for 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. Since our example now includes a version 3 endpoint, let’s update our code to mark version 1 as deprecated:
[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")); } }
Since we are marking version 1 as deprecated, it’s a good idea to also update our builder configuration to set version 2 as the default endpoint:
var apiVersioningBuilder = builder.Services.AddApiVersioning(o => { o.AssumeDefaultVersionWhenUnspecified = true; o.DefaultApiVersion = new ApiVersion(2, 0); o.ReportApiVersions = true; o.ApiVersionReader = ApiVersionReader.Combine( new QueryStringApiVersionReader("api-version"), new HeaderApiVersionReader("X-Version"), new MediaTypeApiVersionReader("ver")); });
Now API version 1.0 is deprecated and the client will get this information from the api-deprecated-versions
header in the response:
Versioning in Minimal API
Minimal APIs in .NET are a streamlined way to create HTTP APIs with minimal code and setup. They offer a simplified syntax and reduced boilerplate, making them ideal for microservices and small-scale applications.
We can use versioning in a minimal API.
To do that, we have to add a new NuGet package Asp.Versioning.Http
.
Let’s install it:
PM> Install-Package Asp.Versioning.Http
Now, let’s create three minimal endpoints performing the same work as their controller-based equivalents. To do that, we are going to modify our Program
class.
We start with creating an API version set:
var apiVersionSet = app.NewApiVersionSet() .HasDeprecatedApiVersion(new ApiVersion(1, 0)) .HasApiVersion(new ApiVersion(2, 0)) .HasApiVersion(new ApiVersion(3, 0)) .ReportApiVersions() .Build();
As you can see, we are defining one version set containing information about all API versions, including a deprecated version. We will be passing this object to our endpoints.
Then, we define our minimal API endpoints:
app.MapGet("api/minimal/StringList", () => { var strings = Data.Summaries.Where(x => x.StartsWith("B")); return TypedResults.Ok(strings); }) .WithApiVersionSet(apiVersionSet) .MapToApiVersion(new ApiVersion(1, 0)); app.MapGet("api/minimal/StringList", () => { var strings = Data.Summaries.Where(x => x.StartsWith("S")); return TypedResults.Ok(strings); }) .WithApiVersionSet(apiVersionSet) .MapToApiVersion(new ApiVersion(2, 0)); app.MapGet("api/minimal/v{version:apiVersion}/StringList", () => { var strings = Data.Summaries.Where(x => x.StartsWith("C")); return TypedResults.Ok(strings); }) .WithApiVersionSet(apiVersionSet) .MapToApiVersion(new ApiVersion(3, 0));
We assign our version set to each of them by using the WithApiVersionSet()
method. This method associates a given endpoint with the version set object that we created previously.
Finally, we are using the MapToApiVersion()
method to specify the version of the endpoint. This method maps a particular endpoint to a specific version. In our case, we have three endpoints with the same URL but different versions, the first is for version 1.0, the second is for version 2.0, and the third is for version 3.0.
We can verify that our endpoints are working as expected.
When we call version 1.0 of the endpoint, it returns all the summaries data which starts with “B”:
Version 2.0 of the same endpoint, returns all the summaries data which starts with “S”:
Finally, version 3.0 of the same endpoint with URI versioning, will return all the summaries data which starts with “C”:
Once again we marked version 1.0 as deprecated. Hence the header api-deprecated-versions
in the response with return version 1.0:
Conclusion
APIs have to evolve, and different versions will start to co-exist. 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.
In this article, we’ve learned about what is versioning, how to configure it, and how to use different versioning schemas in our project. Also, we learned about the way to add deprecated versions and versioning in Minimal APIs.
Thanks for the article ! Juste one thing, for
services.AddVersionedApiExplorer()
to work, you also need the<a href="https://www.nuget.org/packages/Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer/4.1.1?_src=template" target="_blank" style="color: var(--theme-link-color);">Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer</a>
nuget package 🙂Hi Tinathnath. You are correct. And if you inspect that package, you will see that it contains Microsoft.AspNetCore.Mvc.Versioning package, so the package you recommended is just enough to be installed. The author probably installed the MVC package first but then forgot to mention the second one in the article because it is installed (you can check the source code). Thanks for the suggestion.
Yes probably ! And indeed I saw in the source code that both packages are referenced in .csproj. Also I didn’t notice someone already made this comment, sorry for the duplicate !
Great article. I’m not able to add the authentication in the v2.0 version using bearer tocken.
Api version 2.0 works well in anonymous but receive error 401 if authentication is enabled.
Api version 1.0 works well.
Nice day,
Lorenzo (Italy)
Hi Lorenzo.
Thank you for the kind words.
About your issue, I am not sure why is that. I’ve just tested our code from our API book, and authorization works without issues in the V2 controller.
I have developed an api that use these versioning flow and it will be consumed by a flutter mobile app. The app developers are asking me if the api versioning can be with 3 digists insted of two. Example: 1.0.0 instead of 1.0
Can you or anyone share a link to achive this?
Hello Jova. To be honest I never needed something like that (3-digit version number) but as much as I can see the ApiVersion class has the static Parse or TryParse methods. You can inspect them because they accept the string as a parameter instead of two integers as major and minor version numbers. So you may write something like this:
opt.DefaultApiVersion = ApiVersion.Parse(“1.0.0”);
Maybe this will work.
For AddVerionedApiExplorer you have to add following nuget package in your project
Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer
I agree. If you inspect the source code, you will see it is there:
Excelent my friend….
Tank very much, tank you!!!!
You are most welcome. I’m glad you like it.
Nice article. Could you add a Swagger here?
Hello Dan. We will see about that, but I’ve added a few replies to our readers’ comments regarding the same thing, so you might want to check that out. Of course, we have some articles about swagger: https://code-maze.com/swagger-ui-asp-net-core-web-api/ and authorization with swagger: https://code-maze.com/swagger-authorization-aspnet-core/ if that can help you in combination with my replies here.
I remember the Dll hell versioning.
How is it possible that the same solution defines three different API versions with controllers having the same class name?
Well, that’s what versioning is for. As we have shown, you can use either query string params or headers, or you can use URI with versioning that will change the URI of each controller. If you don’t provide any of those, the request will be sent to a default version you’ve set up in the configuration.
I also have the same question. Should we group versions in separate namespaces?
Hello Maurizio. If you inspect our source code (the link is at the start of the article – red box) you will see that we did exactly that. Of course, this is not something I can tell: “Well, this is the best practice, don’t use it any other way”, but it is a good practice to have them separated. Again, the structure of our projects is something we all discuss with our teammates and it can vary from project to project.
If possible can you share a full pledged project, where in the version is spanning across multiple layers for example Business Layer, Database Layer etc. In your example versioning is limited to Controller level. So lets assume there are is a new logic for an Action Method (which will include changes in Business layer and Database layer as well) that we would like to provide in the newest version but the older versions must run as well since it is being used by some clients. So in that case do we end up writing sort of duplicate code for each version with few tweaks here and there?
And what about swagger for accept this old feature in net core 6.0?
I will just repeat the same answer as for the previous comment: “It works great as long as you have versioning attributes on the controller level (not set them up in the configuration) and also you have to use the [MapToApiVersion(“version”)] attribute on top of each action. Of course, you have to configure your swagger to look at all the versions.”
In our Web API book, we have all covered with .NET 6 including versioning and Swagger, and it works great, just you need to add this additional attribute on the action.
Thanks for the article. I wonder how this kind of version declaration will be reflected in swagger generated client.
It works great as long as you have versioning attributes on the controller level (not set them up in the configuration) and also you have to use the [MapToApiVersion(“version”)] attribute on top of each action. Of course, you have to configure your swagger to look at all the versions.