ASP.NET Core API - Path Versioning + Swashbuckle
Tags: dotnet, csharp, webdev
Published: 2020-11-09 08:15:10

# The Specifications

[.NET Core](https://en.wikipedia.org/wiki/.NET_Core) and [ASP.NET Core](https://en.wikipedia.org/wiki/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](https://media.giphy.com/media/jkSvCVEXWlOla/source.gif)

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

```csharp
[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.

```csharp
[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");
}
```
![so cool](https://media.giphy.com/media/jba8ucWVAhG9VcUkx9/source.gif)

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

```csharp
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](https://www.postman.com/) or [cURL](https://curl.se/). 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](https://media.giphy.com/media/J5xVN35LXqTyE/source.gif)

# [Swagger Generation](https://swagger.io/docs/) with Swashbuckle 

Now we'll focus on getting versioning working with **Swagger Generation** and [Swashbuckle](https://github.com/domaindrivendev/Swashbuckle.AspNetCore).

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

```csharp
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 

```csharp
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:

```csharp
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:

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/5c2bv2gdvd62gt7hzv7h.PNG)

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/n5agdpmw4vk5t22eiah3.PNG)

# Small trick

Within your `Properties/launchSettings.json` add the following profiles.

```json
  "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.

![peace out](https://media.giphy.com/media/QoesEe6tCbLyw/source.gif)

[SpaceEmporium Versioning App on Github](https://github.com/henrick-tissink/SpaceEmporium)