If you are maintaining or developing an API, you need to make sure it is versioned.
This ensures that any (breaking) change you need to make does not force parties that use your API to make changes simultaneously and prevents a release hell where every party involved must release a new version at the same time.
Using versioning, you can make the changes in a new version of your API, re-using logic already in place but not touching any of the existing controllers and models so that existing endpoints keeps functioning.
This post describes how to setup a versioned API for .NET Core, from scratch.
I will also show you the steps needed to publish your versioned API to Azure API Management.
Project Setup
Let’s start off with a fresh, .NET Core, Web API application. First thing is to add the necessary packages:
Install-Package Swashbuckle.AspNetCore
Install-Package Swashbuckle.AspNetCore.Swagger
Install-Package Microsoft.AspNetCore.Mvc.Versioning
Install-Package Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer
As always, it’s best to keep your project nice 'n tidy, so create folders for each API version that you want to have available.
Decorate your controllers with these attributes:
[ApiVersion("1.0")]
[Route("api/v{version:apiVersion}/[controller]/[action]")]
[ApiController]
where you substitute ‘1.0’ with the version that you want to communicate to the outside world for that particular controller.
Startup.cs
Within the Configure()
method, add these lines:
services.AddApiVersioning(options => { options.ReportApiVersions = true; });
services.AddVersionedApiExplorer(options =>
{
options.GroupNameFormat = "'v'VVV";
options.SubstituteApiVersionInUrl = true;
});
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo { Title = "Versioned API version 1", Version = "v1" });
c.SwaggerDoc("v2", new OpenApiInfo { Title = "Versioned API version 2", Version = "v2" });
});
Then, within ConfigureServices()
method, add an IApiVersionDescriptionProvider
parameter to the method’s signature:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IApiVersionDescriptionProvider provider)
and add these 2 extension methods:
app.UseSwagger();
app.UseSwaggerUI(
options =>
{
foreach (var description in provider.ApiVersionDescriptions)
{
options.SwaggerEndpoint($"/swagger/{description.GroupName}/swagger.json", description.GroupName);
}
});
As I’d like this API to be available through Azure API Management, we need to make sure that we have the URL of our backend web application present in the Swagger file. We also make 1 little change to the swagger file output when it is being generated during our DevOps build, which is removing part of the URL that has the version in it, as API Management will already take care of this.
To modify the output of our Swagger file, we need to add a Swashbuckle IDocumentFilter
, so add this class to your project:
public class DocumentFilter : IDocumentFilter
{
private readonly IHttpContextAccessor _httpContextAccessor;
public DocumentFilter(IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
}
public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)
{
string url;
var request = _httpContextAccessor.HttpContext?.Request;
if (request != null)
{
url = $"{request.Scheme}://{request.Host}";
}
else
{
url = "https://localhost";
// we need to modify the Key, but that is read-only so let's just make a copy of the Paths property
var copy = new OpenApiPaths();
foreach (var path in swaggerDoc.Paths)
{
var newKey = Regex.Replace(path.Key, "/api/v[^/]*", string.Empty);
copy.Add(newKey, path.Value);
}
swaggerDoc.Paths.Clear();
swaggerDoc.Paths = copy;
}
swaggerDoc.Servers.Add(new OpenApiServer { Url = url });
}
}
This IDocumentFilter
filter we’ve just created needs to be enabled within AddSwaggerGen()
extension:
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo { Title = "Versioned API", Version = "v1" });
c.SwaggerDoc("v2", new OpenApiInfo { Title = "Versioned API", Version = "v2" });
c.DocumentFilter<AddServerDocumentFilter>();
});
And finally, we need to register the httpContextAccessor that we use in our DocumentFilter, in ConfigureServices()
, like so
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
Build your solution and run it locally to check if everything works as expected.
Check the Swagger file too, and check whether there is a url in the servers array:
Notice that our paths array property still contains the version number. We’re only modifying the Swagger output when there is no HTTP request available (using HttpContextAccessor), i.e. when running CLI in our DevOps pipeline.
If all is well, if you browse to /swagger, you should be able to see the different versions that you’ve added, along with all their endpoints. It could look something like this:

That is pretty cool, huh?!
Run Swagger commands during build
In order to be able to run Swagger commands like ‘dotnet swagger tofile’ during the build in DevOps, we need to add an additional JSON file to the root of our solution. Put the following in a file called ‘dotnet-tools.json’ in the root folder of your solution:
{
"version": 1,
"isRoot": true,
"tools": {
"swashbuckle.aspnetcore.cli": {
"version": "5.5.0",
"commands": [
"swagger"
]
}
}
}
For more info on running tools, check out the Microsoft documentation on using tools.
Build Setup
Setup your build pipeline as you would for any .NET Core web application. Next to the regular build steps, we need to add the rendered Swagger files to the artifact as well.
This makes it much easier to setup the release pipeline in the next step, as you don’t have to fetch the Swagger file from some URL, which might be protected or shield off in some way.
To set this up, add a task that installs the needed tool, add a task to create the folder which will contain the generated Swagger files and add a task to generate the actual file, per API version.
Install tool
As we will be running a Swagger command during build in one of the next steps, we need to install the appropriate tool for this. This is what is actually configured in the dotnet-tools.json file we saw earlier.
- task: PowerShell@2
displayName: Install dotnet tools
inputs:
targetType: 'inline'
script: 'dotnet tool restore
Create folder during build
Add a task that will create a folder to hold the Swagger files that we generate.
We will be fetching the files from this location in our release pipeline.
- task: PowerShell@2
displayName: Create a folder that will contain swagger files
inputs:
targetType: 'inline'
script: 'New-Item $(build.artifactstagingdirectory)\OpenAPI -ItemType Directory -Force'
Generate Swagger files for each API version
For each of the versions in your API, add a task that will call the Swagger tool to generate the corresponding Swagger file and put it in the directory we created in the previous step.
- task: PowerShell@2
displayName: Create V1 API OpenAPI specification file
inputs:
targetType: 'inline'
script: 'dotnet swagger tofile --output $(build.artifactstagingdirectory)\OpenAPI\api.v1.json pathToYour.dll v1'
- task: PowerShell@2
displayName: Create V2 API OpenAPI specification file
inputs:
targetType: 'inline'
script: 'dotnet swagger tofile --output $(build.artifactstagingdirectory)\OpenAPI\api.v2.json pathToYour.dll v2'
If you take a look at the generated Swagger files in the artifact, you will see that the paths array now contains modified URL’s, without the version number. This is important, as API Management will already put a version number in the endpoint URL for us and obviously we don’t want a duplicated version number in our URL’s cause that will make us look like n00bs:
Release Setup
There are a couple of dedicated tasks for communicating with Azure’s API Management, which help you to create and later update the API endpoints.
As you saw in the previous step, we generate the swagger file during the build so we can use it in our deployment pipeline. This has one drawback though: the server URL configured in the swagger file (‘https://localhost’ in the example above) will not match with the final URL of the web application that we are deploying to, in a DTAP scenario. So we not only have to deploy our application and update API Management, we also have to make sure we modify the swagger file just before sending it over to API Management.
These are the necessary steps that we need to execute:
- deploy the application
- fetch your generated swagger files from the build artifact
- replace the URL in the servers array with the correct backend URL of your API
- create or update the related product in API Management
- create or update each of the API versions in API Management
1. Deploy the application
This step is pretty straight forward; depending on your hosting setup, deploy the package to the appropriate environment.
2. Fetch generated swagger files from the build artifact
This task is pretty straight forward; it just gets the build artifact.
3. Replace the app service URL in the Swagger files
This task is also pretty self-explanatory; you need to specify the path to the file, the JSON property to change (in our case it’s ‘servers[0].url’) and the value it needs to be replaced with. Notice that we put the version in the service URL here.
The only difference between these 2 tasks is the path to the file and the version in the backend application URL.
4. Create/Update product in API Management
In this step you will create a new product or update an existing one, in API Management. First, you need to specify the correct API Management environment that you want to update, by specifying the subscription, resource group and portal. You can then choose the product that you want to update, or set the name of a new product to be created. You need to set related groups and, if needed, you can even specify the complete policies XML.

5. Create/Update versioned API in API Management
This is the step where we actually create or update a version of our API in API Management. First, specify the correct API Management portal. Specify the OpenApi version and the format of your swagger file, which is JSON. Make sure to tick ‘Product created by previous task’, as we have a task specifically for this.
As our API definition is part of the artifact, specify this as the definition location. You will then need to specify the path to the definition. Fill in a name and display name for your API, and specify the version that this task should create or update. Finally, you need to set the versioning scheme, which is ‘Path’ in our case, and set the URL suffix which will become part of the API Management URL of your API. If needed, you can specify the policies XML.
