API Versioning with ASP.NET and Swagger UI

Learn how to add version support to your ASP.NET Swagger API in a fast, clean and elegant way using Asp.Versioning.Mvc

Stay awhile and listen… As applications grow in complexity and functionalities, so does the necessity for a robust and flexible versioning strategy. API versioning plays a crucial role in ensuring that applications can adapt to changes while maintaining compatibility with existing clients.

In a previous post, I explained how to reduce the boilerplate code by configuring a bootstrap project, set up and run web API projects using extensions and the .NET built-in dependency injection. In this post, I’ll use the same code to implement API Versioning.

When you should use versioning

I would say “always”. However, in practice, versioning is mainly required when you need backward compatibility between application states or modifications. For instance, when you’ve got a system that provides services to another system, such as REST APIs that provide database access to some front-end application. Therefore, you must guarantee compatibility with older API versions otherwise, you’ll have to cascade modifications to client systems each time you change your API interface.

Implement Versioning

A powerful tool that has emerged to address this challenge is the Asp.Versioning.Mvc, a feature-rich library designed to simplify the process of versioning APIs within the ASP.NET Core ecosystem.

Installing Packages

Microsoft provided a couple of packages that will preconfigure the pipeline to handle the requests, recognize route information and deliver it to the versioned controller and action.

To install these packages, use the Visual Studio Nuget package manager or run the following command in the Commons project:

If you are using .NET before version 6, install these (deprecated):

dotnet add package Microsoft.AspNetCore.Mvc.Versioning
dotnet add package Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer

If you are using .NET after version 6, install these:

dotnet add package Asp.Versioning.Mvc
dotnet add package Asp.Versioning.Mvc.ApiExplorer

Modify Controllers / Actions

At both of the API projects, add v{version:apiVersion} to the route string of the MyApiController class.

[ApiController, Route("api/v{version:apiVersion}/[controller]/[action]")]
public class MyApiController : ControllerBase

It’s also possible to add [ApiVersion("1.0")] to the class or to the method.

[ApiVersion("1.0")]
[HttpGet]
public async Task<IActionResult> Get()
{
    return Ok("SimpleApi1 - GET");
}

This procedure will mark the controller or individual methods with the required version.

Register API Versioning Service

Add the following code blocks to the ServiceExtensions class at the Commons project.

var apiVersioningBuilder = services.AddApiVersioning(options =>
{
    options.DefaultApiVersion = new ApiVersion(1, 0);
    options.AssumeDefaultVersionWhenUnspecified = true;
    options.ReportApiVersions = true;
    options.ApiVersionReader = ApiVersionReader.Combine(
        new UrlSegmentApiVersionReader(),
        new QueryStringApiVersionReader("x-api-version"),
        new HeaderApiVersionReader("x-api-version")
    );
});

DefaultApiVersion: sets the default API version applied to services that do not have explicit versions.

AssumeDefaultVersionWhenUnspecified: True if the default API version should be assumed when a client does not provide an API version; otherwise, false.

ReportApiVersions: HTTP headers “api-supported-versions” and “api-deprecated-versions” will be added to all valid service routes.

ApiVersionReader: this object sets the name of headers and URI parameter name from which the API version will be inferred. ApiVersionReader.Combineis just an aggregator to set up multiple objects.

Register API Explorer Service

You will need to add the ApiExplorer configuration. This piece of code defines the conventions and recognizes the API version information, configured in controllers and actions.

apiVersioningBuilder.AddApiExplorer(options =>
{
    options.GroupNameFormat = "'v'VVV";
    options.SubstituteApiVersionInUrl = true;
});

GroupNameFormat: is the template for the version identification. See more here.

SubstituteApiVersionInUrl: sets a value indicating whether the API version parameter should be substituted in route templates.

ApiExplorer and Swagger middleware

Now we need to tell Swagger how to use the metadata gathered by the ApiExplorer. To address this, we need to create a class that implements IConfigureNamedOptions<SwaggerGenOptions>.

I created a new project folder called Components to hold the next two classes and organize the project.

This class will create one OpenApiInfo for each version configured in controllers and/or actions, thus one swagger.json will be generated for each version too.

public class SwaggerConfigureOptions : IConfigureNamedOptions<SwaggerGenOptions>
{
    private readonly IApiVersionDescriptionProvider _provider;
    private readonly ApiConfig _apiConfig;

    public SwaggerConfigureOptions(IApiVersionDescriptionProvider provider, ApiConfig apiConfig)
    {
        _provider = provider;
        _apiConfig = apiConfig;
    }

    public void Configure(SwaggerGenOptions options)
    {
        foreach (var description in _provider.ApiVersionDescriptions)
        {
            options.SwaggerDoc(description.GroupName, 
                description.IsDeprecated
                ? new OpenApiInfo()
                {
                    Title = _apiConfig.Name,
                    Version = description.ApiVersion.ToString()
                }
                : new OpenApiInfo()
                {
                    Title = _apiConfig.Name,
                    Version = description.ApiVersion.ToString(),
                    Description = " This version was deprecated."
                });
        }
    }

    public void Configure(string name, SwaggerGenOptions options) => Configure(options);
}

Without this class, you will get a 404 error because the swagger.json will not be created for versions other than V1.

ApiConfig is a simple singleton class that holds the API name.

public class ApiConfig
{
    public string Name { get; set; }
}

These two classes need to be registered in the services after the apiVersioningBuilder.AddApiExplorer(... call.

services.AddSingleton(new ApiConfig() { Name = serviceName });
services.ConfigureOptions<SwaggerConfigureOptions>();

Commons Project Modifications

We need to change some bits of code to pass on the API name, which will appear in Swagger UI.

The methods RegisterSwaggerServices and ConfigureSwaggerPipeline will need one more string parameter called serviceName.

public static IServiceCollection RegisterSwaggerServices(this IServiceCollection services, string serviceName)

public static WebApplication ConfigureSwaggerPipeline(this WebApplication webApp, string serviceName)

The parameter serviceName configures the Swagger service name, for instance, SimpleApi1 and SimpleApi2.

Also, change the Run method at the Boot class to also receive the serviceName parameter.

public static void Run(string[] args, string serviceName)

Now, at the Program.cs of each API project, we have to call the Run method with one more parameter, like so:

Boot.Run(args, "SimpleApi1"); // for the SimplaApi1 project
Boot.Run(args, "SimpleApi2"); // for the SimplaApi2 project

At the class WebApplicationExtensions, change the method ConfigureSwaggerPipeline. Now, the UseSwaggerUI call must use the metadata versions to create the Swagger UI dropdown.

webApp.UseSwaggerUI(options =>
{
    var versionDescriptor = webApp.Services.GetRequiredService<IApiVersionDescriptionProvider>();
    foreach (var descriptor in versionDescriptor.ApiVersionDescriptions)
    {
        options.SwaggerEndpoint($"/swagger/{descriptor.GroupName}/swagger.json", $"{serviceName} {descriptor.GroupName}");
    }
});

At this point, you should be able to run the projects, browse the Swagger UI page and see the generated version dropdown.

API version selector at Swagger UI

Practical Example

[ApiController]
[Route("api/v{version:apiVersion}/[controller]/[action]")]
public class MyApiController : ControllerBase
{
    [ApiVersion("1.0")]
    [HttpGet]
    public async Task<IActionResult> Get()
    {
        return Ok("SimpleApi1 - GET");
    }
    
    [HttpGet, ApiVersion("2.0")]
    public async Task<IActionResult> GetV2()
    {
        return Ok("SimpleApi1 V2 - GET");
    }
}

This controller have two actions, one registered as v1.0 and other as v2.0. Both actions will be mapped to the correspondent versions at the Swagger UI.

Actions v1.0
Actions v2.0

Yet, it is possible to split the controller into other controllers grouped by action versions. For instance, MyApiV1Controller will have all v1.0 actions and MyApiV2Controller will have all v2.0 actions.

[ApiController, ApiVersion("1.0")]
[Route("api/v{version:apiVersion}/[controller]/[action]")]
public class MyApiV1Controller : ControllerBase
{
    [HttpGet]
    public async Task<IActionResult> Get()
    {
        return Ok("SimpleApi1 - GET");
    }
}

[ApiController, ApiVersion("2.0")]
[Route("api/v{version:apiVersion}/[controller]/[action]")]
public class MyApiV2Controller : ControllerBase
{
    [HttpGet]
    public async Task<IActionResult> GetV2()
    {
        return Ok("SimpleApi1 V2 - GET");
    }
}

Note that only the controller has the attribute [ApiVersion("XX")]. Thus all its actions will have the same version.

Conclusion

We’ve navigated through the key concepts of API versioning, from understanding the importance of versioning to implementing it seamlessly within the ASP.NET Core ecosystem. By adopting a versioning strategy, developers can ensure that their applications remain flexible and adaptable, accommodating changes without disrupting existing clients.

Get the source: https://github.com/raffsalvetti/ApiVersioning

See ya!