Henrick Tissink
Posted on November 9, 2020
The Specifications
.NET Core and ASP.NET Core have come a long way - and so have the different tools to version your ASP.NET Core APIs.
There are three main ways to version your API endpoints
- Path versioning version
www.some-site.com/v1/potatoes
- Query String
www.some-site.com?api-version=1.0
- Http Header
api-supported-version: 1.0
in the request header
Path Versioning
The request path is an address for a resource. When versioning a resource, versioning in the request path makes the most sense - it's declarative, readable, and expresses the most meaning.
A Simple Implementation
First create a new ASP.NET Core API project, and add the following Nuget Package
Microsoft.AspNetCore.Mvc.Versioning
Now, create a controller
and add the following endpoints
[Route("api/[controller]")]
[ApiController]
public class SpaceEmporiumController : ControllerBase
{
[HttpGet("MoonRocks")]
public IActionResult GetMoonRocks() => Ok("Here are some Moon Rocks! v1");
[HttpGet("MoonRocks")]
public IActionResult GetMoonRocksV2() => Ok("Here are some Moon Rocks! v2");
}
Now there are two endpoints to get moon rocks - a version 1, and a newer version 2. ASP.NET Core has no idea how to setup these endpoints because currently they have the same path (and REST verb), viz.
GET
api/SpaceEmporium/MoonRocks
We can now use Microsoft.AspNetCore.Mvc.Versioning to indicate which versions go where.
[Route("api/v{version:apiVersion}/[controller]")]
[ApiVersion("1")]
[ApiVersion("2")]
[ApiController]
public class SpaceEmporiumController : ControllerBase
{
[MapToApiVersion("1")]
[HttpGet("MoonRocks")]
public IActionResult GetMoonRocks() => Ok("Here are some Moon Rocks! v1");
[MapToApiVersion("2")]
[HttpGet("MoonRocks")]
public IActionResult GetMoonRocksV2() => Ok("Here are some Moon Rocks! v2");
}
A few simple things are happening here
The attribute
[Route("api/v{version:apiVersion}/[controller]")]
tells ASP.NET Core to add the version attribute to the request path.The attribute
[ApiVersion("x")]
specifies the API versions present on this controllerThe attribute
[MapToApiVersion("x")]
maps an endpoint to the specific version
We now have two endpoints:
GET
api/v1/SpaceEmporium/MoonRocks
GETapi/v2/SpaceEmporium/MoonRocks
returning
"Here are some Moon Rocks! v1"
"Here are some Moon Rocks! v2"
respectively.
For our convenience we'll add one last thing. Within your Startup.cs file add
public void ConfigureServices(IServiceCollection services)
{
services.AddApiVersioning(o =>
{
o.AssumeDefaultVersionWhenUnspecified = true;
o.DefaultApiVersion = new ApiVersion(1, 0);
});
}
The .ApiVersioning
options are now configured so that we don't have to configure versioning throughout our entire application. If we don't specify versioning on the controllers or endpoints, then it is assumed to be version 1.
Now run the application and test it using Postman or cURL. You should be able to run the following cURL commands, with responses respectively:
curl -X GET https://localhost:5001/api/v1/SpaceEmporium/MoonRocks
Here are some Moon Rocks! v1curl -X GET https://localhost:5001/api/v2/SpaceEmporium/MoonRocks
Here are some Moon Rocks! v2
Great! Our API is now versioned - the code is easy to read and maintain, and the path versioning it generates is easy to understand and use.
Swagger Generation with Swashbuckle
Now we'll focus on getting versioning working with Swagger Generation and Swashbuckle.
First, add these Nuget Packages:
- Swashbuckle.AspNetCore.Swagger
- Swashbuckle.AspNetCore.SwaggerGen
- Swashbuckle.AspNetCore.SwaggerUI
Now, to get Swashbuckle to understand what's actually going on we'll need to implement two filters:
- RemoveVersionFromParameter
- ReplaceVersionWithExactValueInPath
RemoveVersionFromParameter
removes the version parameter from the Swagger doc that's generated as a result of the [Route("api/v{version:apiVersion}/[controller]")]
attribute.
ReplaceVersionWithExactValueInPath
replaces the version variable in the path, with the exact value e.g. v1, v2.
public class RemoveVersionFromParameter : IOperationFilter
{
public void Apply(OpenApiOperation operation, OperationFilterContext context)
{
if (!operation.Parameters.Any())
return;
var versionParameter = operation.Parameters.Single(p => p.Name == "version");
operation.Parameters.Remove(versionParameter);
}
}
and
public class ReplaceVersionWithExactValueInPath : IDocumentFilter
{
public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)
{
var paths = new OpenApiPaths();
foreach(var (key, value) in swaggerDoc.Paths)
paths.Add(key.Replace("v{version}", swaggerDoc.Info.Version), value);
swaggerDoc.Paths = paths;
}
}
In your Startup.cs
file, add the following:
public void ConfigureServices(IServiceCollection services)
{
services.AddApiVersioning(o =>
{
o.AssumeDefaultVersionWhenUnspecified = true;
o.DefaultApiVersion = new ApiVersion(1, 0);
});
services.AddControllers();
services.AddSwaggerGen(configureSwaggerGen);
}
private static void configureSwaggerGen(SwaggerGenOptions options)
{
addSwaggerDocs(options);
options.OperationFilter<RemoveVersionFromParameter>();
options.DocumentFilter<ReplaceVersionWithExactValueInPath>();
options.DocInclusionPredicate((version, desc) =>
{
if (!desc.TryGetMethodInfo(out var methodInfo))
return false;
var versions = methodInfo
.DeclaringType?
.GetCustomAttributes(true)
.OfType<ApiVersionAttribute>()
.SelectMany(attr => attr.Versions);
var maps = methodInfo
.GetCustomAttributes(true)
.OfType<MapToApiVersionAttribute>()
.SelectMany(attr => attr.Versions)
.ToList();
return versions?.Any(v => $"v{v}" == version) == true
&& (!maps.Any() || maps.Any(v => $"v{v}" == version));
});
}
private static void addSwaggerDocs(SwaggerGenOptions options)
{
options.SwaggerDoc("v1", new OpenApiInfo
{
Version = "v1",
Title = "Space Emporium API",
Description = "API for the Space Emporium",
});
options.SwaggerDoc("v2", new OpenApiInfo
{
Version = "v2",
Title = "Space Emporium API",
Description = "API for the Space Emporium",
});
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
...
app.UseSwagger(c => { c.RouteTemplate = "dev/swagger/{documentName}/swagger.json"; });
app.UseSwaggerUI(options =>
{
options.SwaggerEndpoint("/dev/swagger/v1/swagger.json", "Space Emporium API v1");
options.SwaggerEndpoint("/dev/swagger/v2/swagger.json", "Space Emporium API v2");
options.RoutePrefix = "dev/swagger";
});
...
}
This will generate a v1 and a v2 Swagger doc. Hosted at localhost:5001/dev/swagger
.
Both Swagger docs should now work and you should see:
Small trick
Within your Properties/launchSettings.json
add the following profiles.
"profiles": {
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"launchUrl": "dev/swagger",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"SpaceEmporium": {
"commandName": "Project",
"launchBrowser": true,
"launchUrl": "dev/swagger",
"applicationUrl": "https://localhost:5001;http://localhost:5000",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
This will launch your application straight onto the Swagger page.
Closing
Path versioning in ASP.NET Core is powerful and easy to implement. Once you have Swagger Generation implemented with versioning it gives that versioning power to whoever is consuming your APIs through the relevant Swagger docs.
Posted on November 9, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
September 29, 2024