Dealing with default API versions in Swagger UI
Image from Pexels by Adonyi Gábor

Dealing with default API versions in Swagger UI

Fixing additional routes in Swagger API when using default versions

Having your REST API versioned is important for evolving of the service over time. Especially if your service is exposed t multiple 3rd party clients. Once you decide that API signature needs to be updated and changed, all of your clients need to adapt to that change.

Unfortunately not all clients can keep up the same pace of changes and you need to provide them a short or long term backward compatibility with the old version of your service. For this purpose it is important to support API versioning in your REST API.

I will skip the versioning part in this article and focus only on case when you have default version in place. However, you can check out the article supporting project from GitHub to see the whole versioning and Swagger setup

The most common and at the same time my favorite approach for handling versions is using version as a route segment. So for my first version (1.0) I will have all my routes prefixed with v1 segment, for example /v1/users/. Now you may want to support both /v1/users and /users routes to go to the same endpoint.

In short, your routes will point to following versions:

  • /v1/users/ -> v1.0
  • /users/ -> v1.0

You might ask why would I do that? Well it is pretty simple. Assume that all your clients are using specific version, but regrdless you want to expose your endpoints without version segment, so when the time comes and you release youe second version (2.0) you can set default version to 2.0 and all routes without the specific version (/users/) will automatically go to v2 controllers while you can still target specific version with version segment in the route.

Now, with new version in place your routes can target versions as following:

  • /v1/users/ -> 1.0
  • /v2/users/ -> 2.0
  • /users/ -> 2.0

You can also decide to keep default version routing to 1.0 as long as your 2.0 of API is not completely ready. The point is you have options to offer to your service clients in this transition period.

Note

API versions are represented only with a major version. There is no minor version involved in API versioning, so something like 1.11 is not supported as intention of versioning is not to have fine grained versions as each is considered as a braking change to the outside works as the signature and behavior are changed

Implementation

First thing first, let's just quickly setup Swagger and versioning in our simple Sample API.

Step one, add necessary nuget packages to API project

<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <TargetFramework>netcoreapp3.1</TargetFramework>
    <Description>Sample.DefaultVersion.Api</Description>
    <ProjectGuid>{92B9F388-4545-4FEE-8AC3-CBB110E138BD}</ProjectGuid>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer" Version="4.1.1" />
    <PackageReference Include="Swashbuckle.AspNetCore" Version="5.4.1" />
    <PackageReference Include="Swashbuckle.AspNetCore.Annotations" Version="5.4.1" />
    <PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="5.4.1" />
  </ItemGroup>

</Project>

    

Now when all necessary packages are added, we can proceed with dependency injection (services) registration in Startup.cs method ConfigureServices

Note

All code from this article and sample asp,net core project is available from GitHub public repository https://github.com/dejanstojanovic/swagger-default-version

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddControllers();

            #region Swagger and versioning services

            services.AddVersionedApiExplorer(options =>
            {
                options.GroupNameFormat = "'v'VVV";
                options.SubstituteApiVersionInUrl = true;
                options.AssumeDefaultVersionWhenUnspecified = true;
                options.DefaultApiVersion = new ApiVersion(1, 0);
            });

            services.AddApiVersioning(options =>
            {
                options.ReportApiVersions = true;
            });

            services.AddSwaggerGen(options =>
            {
                options.EnableAnnotations();
                using (var serviceProvider = services.BuildServiceProvider())
                {
                    var provider = serviceProvider.GetRequiredService<IApiVersionDescriptionProvider>();
                    String assemblyDescription = typeof(Startup).Assembly.GetCustomAttribute<AssemblyDescriptionAttribute>().Description;
                    foreach (var description in provider.ApiVersionDescriptions)
                    {
                        options.SwaggerDoc(description.GroupName, new Microsoft.OpenApi.Models.OpenApiInfo()
                        {
                            Title = $"{typeof(Startup).Assembly.GetCustomAttribute<AssemblyProductAttribute>().Product} {description.ApiVersion}",
                            Version = description.ApiVersion.ToString(),
                            Description = description.IsDeprecated ? $"{assemblyDescription} - DEPRECATED" : $"{assemblyDescription}"
                        });
                    }
                }

                // integrate xml comments
                var currentAssembly = Assembly.GetExecutingAssembly();
                var xmlDocs = currentAssembly.GetReferencedAssemblies()
                .Union(new AssemblyName[] { currentAssembly.GetName() })
                .Select(a => Path.Combine(Path.GetDirectoryName(currentAssembly.Location), $"{a.Name}.xml"))
                .Where(f => File.Exists(f)).ToArray();

                Array.ForEach(xmlDocs, (d) =>
                {
                    options.IncludeXmlComments(d);
                });
            });

            #endregion
        }
    

Next thing is to add Swagger UI to ASP.NET Core pipeline 

        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            app.UseHttpsRedirection();
            app.UseRouting();

            #region Configure Swagger
            app.UseSwagger();

            var provider = app.ApplicationServices.GetService<IApiVersionDescriptionProvider>();
            app.UseSwaggerUI(
                options =>
                {
                    options.DocExpansion(Swashbuckle.AspNetCore.SwaggerUI.DocExpansion.None);
                    // build a swagger endpoint for each discovered API version
                    foreach (var description in provider.ApiVersionDescriptions)
                        options.SwaggerEndpoint($"/swagger/{description.GroupName}/swagger.json", description.GroupName.ToUpperInvariant());
                });
            #endregion
            app.UseAuthorization();
            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllers();
            });
        }
    }
    

Sample versioned controller

Finally, when whole setup is in place we can add our sample controller. I used simple UserModel class to retrieve list from a controller GET method

namespace Sample.DefaultVersion.Api.Models
{
    public class UserModel
    {
        public Guid Id { get; set; }
        public String Username { get; set; }
        public String FullName { get; set; }
    }
}

    

To support both versioned and non versioned route (which falls back to default version defined in the service registration) I have to decorate the controller with two route parameters, one with version in the route and one without.

This will allow to invoke route from both /v1/users and /user

namespace Sample.DefaultVersion.Api.Controllers.v1
{
    [ApiVersion("1.0")]
    [ApiController]
    [Route("v{version:apiVersion}/[controller]")]
    [Route("[controller]")]
    public class UsersController : ControllerBase
    {
        private readonly ILogger<UsersController> _logger;

        public UsersController(ILogger<UsersController> logger)
        {
            _logger = logger;
        }

        static IEnumerable<UserModel> users = new[]
        {
            new UserModel(){Id=Guid.NewGuid(), Username="john_smith", FullName="John Smith" },
            new UserModel(){Id=Guid.NewGuid(), Username="payton_keenan", FullName="Payton Keenan" }
        };

        [HttpGet]
        public IEnumerable<UserModel> Get()
        {
            return users;
        }
    }
}
    

Problems

Now, first thing you probably noticed is double routes. When you open Swagger UI, you will see that each controller action has to routes in Swagger UI.

Double Routes

One is the route for the versioned endpoint and the second one is the one that uses default version. Although it is using the default version, the endpoint without version segment in the route has one additional parameter named api-version when you expand action in Swagger UI.

Some people may be OK with this since it does not impact your API functionality, but from the documentation point of view, Swagger becomes more clutter and it may cause problems if you are using client generators like Autorest or NSwag.

Api Version Param

Removing api-version parameter from default version route

We do not have to get rid of the default route yet. In fact someone would prefer to have this route in Swagger UI as well, to support version-less route in the documentation so that consumers know that they can use both routes.

Since we already set default version, this parameter is not really needed to be displayed, so let's get rid of it from the UI. This is easily achievable with simple implementation of IOperationFilter interface which comes with Swagger nuget package which we already brought in to our project.

namespace Sample.DefaultVersion.Api.Swagger
{
    public class RemoveQueryApiVersionParamOperationFilter : IOperationFilter
    {
        public void Apply(OpenApiOperation operation, OperationFilterContext context)
        {
            var versionParameter = operation.Parameters
                .FirstOrDefault(p => p.Name == "api-version" && p.In == ParameterLocation.Query);

            if (versionParameter != null)
                operation.Parameters.Remove(versionParameter);
        }
    }
}
    

Now we just need to add RemoveQueryApiVersionParamOperationFilter to SwaggerGen service registration in startup.cs method ConfigureServices

services.AddSwaggerGen(options =>
{
	...
	options.OperationFilter<RemoveQueryApiVersionParamOperationFilter>();
}
    

Api Version No Param

Removing default version route

You may want to get rid of this default version route from Swagger UI because as API grows you documentation may be cluttered. This can be achieved with another type of Swagger filter called document filter. We just need to create our own implementation of IDocumentFilter interface. For the logic which route do we remove, we can use api-version parameter which is generate for this default version route.

namespace Sample.DefaultVersion.Api.Swagger
{
    public class RemoveDefaultApiVersionRouteDocumentFilter : IDocumentFilter
    {
        public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)
        {
            foreach (var apiDescription in context.ApiDescriptions)
            {
                var versionParam = apiDescription.ParameterDescriptions
                     .FirstOrDefault(p => p.Name == "api-version" &&
                     p.Source.Id.Equals("Query", StringComparison.InvariantCultureIgnoreCase));

                if (versionParam == null)
                    continue;

                var route = "/" + apiDescription.RelativePath.TrimEnd('/');
                swaggerDoc.Paths.Remove(route);
            }
        }
    }
}
    

Same way we register operation filter, we register this document filter

services.AddSwaggerGen(options =>
{
	...
	options.DocumentFilter<RemoveDefaultApiVersionRouteDocumentFilter>();
}
    

Now when we run our project and access Swagger UI route, there will be only one route which includes version. Although default version route is not visible in Swagger it is still available, so endpoint can be accessed both from /v1/users and /users, just default version route is not visible in Swagger UI and makes it less cluttered.

Api Version No Versionless

Note

All code from this article and sample asp,net core project is available from GitHub public repository https://github.com/dejanstojanovic/swagger-default-version

References

Disclaimer

Purpose of the code contained in snippets or available for download in this article is solely for learning and demo purposes. Author will not be held responsible for any failure or damages caused due to any other usage.


About the author

DEJAN STOJANOVIC

Dejan is a passionate Software Architect/Developer. He is highly experienced in .NET programming platform including ASP.NET MVC and WebApi. He likes working on new technologies and exciting challenging projects

CONNECT WITH DEJAN  Loginlinkedin Logintwitter Logingoogleplus  Logingoogleplus

JavaScript

read more

SQL/T-SQL

read more

Umbraco CMS

read more

PowerShell

read more

Comments for this article