loading...
Cover image for ASP.NET Core API - Path Versioning

ASP.NET Core API - Path Versioning

htissink profile image Henrick Tissink ・4 min read

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

working

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");
}
Enter fullscreen mode Exit fullscreen mode

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");
}
Enter fullscreen mode Exit fullscreen mode

so cool

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 controller

  • The attribute [MapToApiVersion("x")] maps an endpoint to the specific version

We now have two endpoints:

GET api/v1/SpaceEmporium/MoonRocks
GET api/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);
   });
}
Enter fullscreen mode Exit fullscreen mode

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! v1

curl -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.

spicy

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);
   }
}
Enter fullscreen mode Exit fullscreen mode

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;
   }
}
Enter fullscreen mode Exit fullscreen mode

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";
   });
   ...
}
Enter fullscreen mode Exit fullscreen mode

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:

Alt Text

Alt Text

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"
      }
    }
  }
Enter fullscreen mode Exit fullscreen mode

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.

peace out

SpaceEmporium Versioning App on Github

Discussion

pic
Editor guide