UPDATE
July 27, 2020: This article is outdated. After my collegue discovered this Microsoft repository I decided to re-implement the sample listed in our repo and I will write an update to this article soon.
Links
- Sample Code on GitHub
- Wiki of the versioning-project on GitHub
- Scott Hanselmans post on versioning
- Post by .NET Core Tutorials
- Github repository of Swashbuckle
Why Versioning?
It doesn’t matter how good you plan your API project. At some point in time you eventually run into the point where a breaking change might be inevitable. To be precise: by changes I don’t mean to add a property in the data contract but for instance to rename one. This would be considered a breaking change and this should lead to a new version (see Semantic Versioning for more details on this).
So instead of replacing the existing API you would provide a new version of your facade so that consumers of the former model would be still served. Lets build a little example.
I our sample we have 1 endpoint only:
https://my-api.com/api/Articles
If you send a GET request to this endpoint (I’ll skip authentification and alikes) you are presented with this JSON result:
[{
"id": 12,
"number": "ART-001",
"label": "Cool Stuff"
}]
A compatible change to this would be to add a new property:
[{
"id": 12,
"number": "ART-001",
"label": "Cool Stuff",
"price": 12.00
}]
Consumers of the API which didn’t got the new price
property would be still served with valid data. But what happens, if we rename the price
in our 3rd release?
[{
"id": 12,
"number": "ART-001",
"label": "Cool Stuff",
"totalPrice": 12.00
}]
Now we have a breaking change. Consumers of the second version would fail to deserialze the prize now.
In order to get rid of this problem we provide several URLs matching different versions of our API. So we would start by changing the URL to:
https://my-api.com/api/v1.0/Articles
delivering the outcome of Listing 1,
https://my-api.com/api/v1.1/Articles
for Listing 2 and
https://my-api.com/api/v2.0/Articles
for the result from Listing 3. All endpoints keep being served all the time and thus the consumer has to be aware of the new features and he is the one who decides to change the URL.
Solutiuon in ASP.NET Core
Although the solution should be pretty clear it raises some questions for the realization:
- How can we add versions to URLs in ASP.NET Core?
- How can we differentiate the models used by the different versions?
- How do we configure the routing accordingly?
- How should we configure Swashbuckle so that Swagger knows all about our plan?
Preparation
Lets start simple and create a new ASP.NET Web Application with the template “API App” in Visual Studio (You can follow this article using VS Code too. Simply use dotnet new webapi
.)
The first thing to do is to bring in some NuGets for our project:
install-package Microsoft.AspNetCore.Mvc.Versioning
install-package Swashbuckle.AspNetCore
install-package Swashbuckle.AspNetCore.Annotations
The last line sits there for enabling us to see XML comments in our Swagger documentation (I’m not going to cover this here.).
I then remove the default WeatherForecastController
and WeatherForecastModel
and create a folder structure like this:
Controller and model for version 1.0
Just ignore the files for the moment. The idea is that controllers and models are versioned by simply putting them into different folders. So in the beginning you obviously would start with folders labelled “v1.0” or something similar. The point is, that you start with this right from the beginning.
However lets take a look at the ArticleModel
in version 1.0:
namespace ApiVersioningSample.Models.v1_0
{
public class ArticleModel
{
public long Id { get; set; }
public string Label { get; set; }
public string Number { get; set; }
}
}
This represents the type which delivers the JSON shown in Listing 1. Note that the namespace is poiting to “Models.v1_0”!. Now lets look at our controller in the v1_0
folder:
namespace ApiVersioningSample.Controllers.v1_0
{
...
using Models.v1_0;
[ApiVersion("1.0")]
[ApiController]
[Route("api/v{version:apiVersion}/[controller]")]
public class ArticleController : ControllerBase
{
[HttpGet]
[MapToApiVersion("1.0")]
public IEnumerable<ArticleModel> Get()
{
return new[]
{
new ArticleModel
{
Id = 1,
Number = "ART-001",
Label = "Cool Stuff"
}
};
}
}
}
Here the first part of the magic happens. First of all we decoate the type with some attributes:
ApiVersion
: Lets you define in which versions of your API this controller and all of it’s methods is supported. You can add multiple attributes of this type to one controller. That would mean, that the controller is supported lets say in version 1.0 and 1.1.Route
: We add the part “v{version:apiVersion}” to our route here. We do this because I decided to use the new endpoint routing of ASP.NET Core. If you prefer MVC you could add this at a central point inStartup.cs
too.MapToApiVersion
: This allows us to control which method implementations should be available in which API version. This attribute correspondents with theApiVersion
defined at class-level. So if you define multiple API versions supported by the controller you could also map multiple versions to one method. This would mean that the method is supported in more than one version.
Note also that we use the namespace Models.v1_0
so that the ArticleModel
used in the GET-method will be the correct one.
Configuration
Up to this point nothing of this will work as we need it. The route for instance would simply result in an URL “api/v{version:apiVersion}/Article” which is not what we want. So lets head over to Startup.cs
:
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.AddApiVersioning();
}
Besides the attributes ApiVersion
and MapToApiVersion
we’ve already used in Listing 5 the NuGet package Microsoft.AspNetCore.Mvc.Versioning
(see Listing 3) also provides an extension method AddApiVersioning
. This tells ASP.NET Core to understand our route pattern correctly.
After I added this line I open my launchSettings.json
in the “Properties” folder (different in VS Code!) and change my IIS profile:
{
"$schema": "http://json.schemastore.org/launchsettings.json",
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:1252",
"sslPort": 44320
}
},
"profiles": {
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"launchUrl": "api/v1.0/Article",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}
Line 15 is the important one. If I execute out of Visual Studio now, I see the following result:
So ASP.NET took care of everyting and replaced the version-pattern of my route correctly.
Swashbuckle
Lets bring in some convenience here and enable Swagger. To do this we firstly have to enable it in the Configure
method in Startup.cs
:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(
endpoints =>
{
endpoints.MapControllers();
});
app.UseSwagger();
app.UseSwaggerUI(
c =>
{
c.SwaggerEndpoint("/swagger/v1.0/swagger.json", "codingfreaks API v1.0");
});
}
I’ve shown all of the method so that it is easier for you to reproduce my steps. Lines 15 to 20 are the Swagger-related ones. I simply enable Swagger and tell Swashbuckle where to place my currently only provided version 1.0.
Now the more complicated part inside of ConfigureServices
. After the new line in Listing 6 add the following:
services.AddSwaggerGen(
c =>
{
c.SwaggerDoc(
"v1.0",
new OpenApiInfo
{
Title = "codingfreaks API",
Version = "v1.0"
});
c.EnableAnnotations();
});
finally replace the value “api/v1.0/Article” in launchSettings
(Listing 7) with the word “swagger” and run your project again. The result should look like this:
As you can see the problem is that Swashbuckle knows nothing about our API versioning yet. Swashbuckle relies on code inspection and basically transforms C#-stuff into Swagger. It messes up the routes and puts the “version”-part as a parameter into each method. So we have to help it a little bit.
Step 1 is to add 2 so called filters. Filters are classes implementing certain interfaces so that they can be included into the ASP.NET pipeline and tools like Swashbuckle. Our first filter will be named RemoveVersionParameterFilter
:
public class RemoveVersionParameterFilter : IOperationFilter
{
public void Apply(OpenApiOperation operation, OperationFilterContext context)
{
var versionParameter = operation.Parameters.Single(p => p.Name == "version");
operation.Parameters.Remove(versionParameter);
}
}
What this does is removing the parameter named “version” from all the methods inside each controller.
As you can see this is simply the controller Swashbuckle found. If you have more controllers, it’ll simple find all of them.
Step 2 is to implement an IDocumentFilter
which is responsible for generating the correct path (including the version) in the documentation
public class ReplaceVersionWithExactValueInPathFilter : IDocumentFilter
{
public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)
{
var paths = new OpenApiPaths();
foreach (var path in swaggerDoc.Paths)
{
paths.Add(path.Key.Replace("v{version}", swaggerDoc.Info.Version), path.Value);
}
swaggerDoc.Paths = paths;
}
}
Again here is a breakpoint screenshot of this:
It shows you that we are accessing the routes here. So what we are doing is to replace our pattern “v{version}” with the correspoding Swashbuckle version value.
Those 2 types must now be configured in our Startup.cs
. I post the complete Swashbuckle-part here to be consistent:
services.AddSwaggerGen(
c =>
{
c.SwaggerDoc(
"v1.0",
new OpenApiInfo
{
Title = "codingfreaks API",
Version = "v1.0"
});
c.OperationFilter<RemoveVersionParameterFilter>();
c.DocumentFilter<ReplaceVersionWithExactValueInPathFilter>();
c.EnableAnnotations();
});
I just added the lines 11 and 12. If you run your app now, Swagger looks like this:
I showed you Swagger after I executed the sample request to demonstrate that it is really working.
Adding another version
All this would be done in vain if we would not show at least one more version. So lets do it:
Add another ArticleModel
in the solution folder “Models\1_1”:
Be sure to put it into the correct namespace and to add another property Price
.
namespace ApiVersioningSample.Models.v1_1
{
public class ArticleModel
{
public long Id { get; set; }
public string Label { get; set; }
public string Number { get; set; }
public decimal Price { get; set; }
}
}
Add another ArticleController
in the solution folder “Controllers\1_1”:
Be sure to put it into the correct namespace and use the corresponding models-namespace and define a price for this article.
namespace ApiVersioningSample.Controllers.v1_1
{
...
using Models.v1_1;
[ApiVersion("1.1")]
[ApiController]
[Route("api/v{version:apiVersion}/[controller]")]
public class ArticleController : ControllerBase
{
[HttpGet]
[MapToApiVersion("1.1")]
public IEnumerable<ArticleModel> Get()
{
return new[]
{
new ArticleModel
{
Id = 1,
Number = "ART-001",
Label = "Cool Stuff",
Price = 12
}
};
}
}
}
Reflect changes in Startup.cs
First of all tell Swashbuckle that there is a new version by editing Configure
:
app.UseSwaggerUI(
c =>
{
c.SwaggerEndpoint("/swagger/v1.0/swagger.json", "codingfreaks API v1.0");
c.SwaggerEndpoint("/swagger/v1.1/swagger.json", "codingfreaks API v1.1");
});
I simply added line 5.
Then add the new version to the UI in ConfigureServices
method:
services.AddSwaggerGen(
c =>
{
c.SwaggerDoc(
"v1.0",
new OpenApiInfo
{
Title = "codingfreaks API",
Version = "v1.0"
});
c.SwaggerDoc(
"v1.1",
new OpenApiInfo
{
Title = "codingfreaks API",
Version = "v1.1"
});
c.OperationFilter<RemoveVersionParameterFilter>();
c.DocumentFilter<ReplaceVersionWithExactValueInPathFilter>();
c.EnableAnnotations();
});
Test your changes
When you run your app now you should be able to select different API versions in Swagger (top right) and when you test version 1.1 you should get a price too.
Theres more
My sample code shown here only covers pretty basic configuration options and is not very elegant. My goal was to give you a first understanding of the versioning in ASP.NET Core and Swashbuckle. There are a lot of good posts going into more detail (see Links at the top).
You can for instance control what ASP.NET Core delivers in responses, how to implement default versions if there is no version specified in the URL (fallback if you switch later in your project) and
Conclusion
You should provide API versioning right from the beginning of your project. It’s not that complicated. Maybe put a little bit more effort in it than simply taking my code because it’s not production-ready. But the idea should be clear.