Versioning ASP.Net Core APIs with Swashbuckle - Making Space Potatoes V-x.x.x
Henrick Tissink
Posted on August 26, 2019
Updated Path Versioning Post Here
Creating a new API with ASP.NET Core is fun and easy; versioning that API is a bit harder. The cinch though is how generating Swagger for the initial version is a breeze while generating versioned swagger is a little trickier.
Disclaimer: due to bugs in ASP.Net Core 2.2.0, please use ASP.Net Core 2.1.0
Let's dive into this by creating a simple ASP.Net Core Api (2.1.0) to give us some Space Potatoes. First we'll create a controller called SpaceFarmController
.
[Route("api/[controller]")]
[ApiController]
public class SpaceFarmController : ControllerBase
{
[HttpGet("Potatoes")]
public string SpacePotatoes() => "Space Potatoes v1";
}
Now, let's add some versioning to this little API. Add the following Nuget Package to your project:
- Microsoft.AspNetCore.Mvc.Versioning (version 3.0.0)
Our API will be versioned using Route Versioning, this simply means that our API will be versioned according to its routes, with
version | route |
---|---|
v1 | .../api/controller/v1/something |
V2 | .../api/controller/v2/something |
v{n} | .../api/controller/v{n}/something |
The API can now easily be versioned by adding attributes to the controller:
[ApiVersion("1")]
[Route("api/v{version:apiVersion}/[controller]")]
[ApiController]
public class SpaceFarmController : ControllerBase
{
[HttpGet("Potatoes")]
public string SpacePotatoes() => "Space Potatoes v1";
}
And modifying the Startup.cs
file:
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
services.AddApiVersioning(); // just add this
}
Running your app locally, you'll now be able to get some Space Potatoes by making a GET request to https://localhost:5001/api/v1/spacefarm/potatoes
.
Let's add some Swagger generation to the mix. For this we'll be using Swashbuckle. Add the following Nuget Package to your project:
- Swashbuckle.AspNetCore (version 4.0.1)
Adding Swagger generation to your project is now as simple as adding the following to your Startup.cs
file:
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
services.AddApiVersioning();
// Add this
services.AddSwaggerGen(options =>
options.SwaggerDoc("v1", new Info
{
Version = "v1",
Title = "v1 API",
Description = "v1 API Description",
TermsOfService = "Terms of Service v1"
}));
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
loggerFactory.AddConsole(Configuration.GetSection("Logging"));
loggerFactory.AddDebug();
// And add this, an endpoint for our swagger doc
app.UseSwagger();
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint($"/swagger/v1/swagger.json", $"v1");
});
app.UseMvc();
}
For a final trick, change your launchSettings.json
entry for your application to:
"*YOUR APPLICATION NAME HERE*": {
"commandName": "Project",
"launchBrowser": true,
"launchUrl": "swagger",
"applicationUrl": "https://localhost:5001;http://localhost:5000",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
Now when you run your application, it immediately launches the Swagger page.
If you've done all this correctly, when you run your app you should see a screen similar to:
This is great - you've got Swagger generation and a v1 of your API. But it's an ever changing world in the Space Potato market, and pretty soon you're going to want to create an additional way to get Space Potatoes - a v2.0 if you will.
Now, adding another version is quite easy, but getting the Swagger generation to work with this new version is a bit tricky.
First, let's add a v2 for our endpoint in the SpaceFarmController.cs
:
[Route("api/v{version:apiVersion}/[controller]")]
// Which versions the controller responds to
[ApiVersion("1")]
[ApiVersion("2")]
[ApiController]
public class SpaceFarmController : ControllerBase
{
[HttpGet]
// Which version the route corresponds to
[MapToApiVersion("1")]
[Route("Potatoes")]
public string GetPotatoes() => "Space Potatoes v1";
[HttpGet]
// Which version the route corresponds to
[MapToApiVersion("2")]
[Route("Potatoes")]
public string GetPotatoesV2() => "Space Potatoes v2";
}
Great, fantastic even. We now have two versions of our API. If we run this, we can make requests to https://localhost:5001/api/v1/spacefarm/potatoes
and https://localhost:5001/api/v2/spacefarm/potatoes
(but the Swagger generation will fail).
Swashbuckle doesn't know that there is a difference in the two routes - it sees them as one and the same. So let's help Swashbuckle out. For this we'll need to create two classes:
public class RemoveVersionFromParameter : IOperationFilter
{
public void Apply(Operation operation, OperationFilterContext context)
{
var versionParameter = operation.Parameters.Single(p => p.Name == "version");
operation.Parameters.Remove(versionParameter);
}
}
and
public class ReplaceVersionWithExactValueInPath : IDocumentFilter
{
public void Apply(SwaggerDocument swaggerDoc, DocumentFilterContext context)
{
swaggerDoc.Paths = swaggerDoc.Paths
.ToDictionary(
path => path.Key.Replace("v{version}", swaggerDoc.Info.Version),
path => path.Value
);
}
}
Both classes a types of special filters that Swashbuckle provides to aid Swagger generation. RemoveVersionFromParameter
will remove the API Version as a parameter from the Swagger document. ReplaceVersionWithExactValueInPath
will change the path from being variable api/v{version:apiVersion}/[controller]
to having a fixed path e.g. api/v1/[controller]
.
Finally, the filters will need to be applied to the Swagger generation in Startup.cs
, and some other minor modifications need to be made:
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
services.AddApiVersioning();
services.AddApiVersioning(o =>
{
o.AssumeDefaultVersionWhenUnspecified = true;
o.DefaultApiVersion = new ApiVersion(1, 0);
});
services.AddSwaggerGen(options =>
{
options.SwaggerDoc("v1",
new Info
{
Version = "v1",
Title = "v1 API",
Description = "v1 API Description",
TermsOfService = "Terms of usage v1"
});
// Add a SwaggerDoc for v2
options.SwaggerDoc("v2",
new Info
{
Version = "v2",
Title = "v2 API",
Description = "v2 API Description",
TermsOfService = "Terms of usage v3"
});
// Apply the filters
options.OperationFilter<RemoveVersionFromParameter>();
options.DocumentFilter<ReplaceVersionWithExactValueInPath>();
// Ensure the routes are added to the right Swagger doc
options.DocInclusionPredicate((version, desc) =>
{
var versions = desc.ControllerAttributes()
.OfType<ApiVersionAttribute>()
.SelectMany(attr => attr.Versions);
var maps = desc.ActionAttributes()
.OfType<MapToApiVersionAttribute>()
.SelectMany(attr => attr.Versions)
.ToArray();
return versions.Any(v => $"v{v.ToString()}" == version)
&& (!maps.Any() || maps.Any(v => $"v{v.ToString()}" == version));;
});
});
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
loggerFactory.AddConsole(Configuration.GetSection("Logging"));
loggerFactory.AddDebug();
app.UseSwagger();
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint($"/swagger/v1/swagger.json", $"v1");
// Specify and endpoint for v2
c.SwaggerEndpoint($"/swagger/v2/swagger.json", $"v2");
});
app.UseMvc();
}
Most of what was done is quite self-explanatory. The only part that might be of interest is the options.DocInclusionPredicate
. The little bit of code defining this predicate finds the versions attributed to the controller and the version mappings associated with a particular endpoint, and ensures that the endpoint is included in the correct Swagger doc.
If everything has been done correctly, running the application should now launch a Swagger doc with multiple versions,
And routes for each version:
Posted on August 26, 2019
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.