Setting up Swagger to support versioned API endpoints in ASP.NET Core

Adding Swagger support for versioned REST Web API endpoints

Versioning of your endpoints is important especially if you have 3rd party dependent clients of your REST API service. Any change in your endpoint, for example in data structure may impact clients even if it is backward compatible, clients may process your endpoint data in different ways, so even adding one additional property to your model may also impact functionality of the client which is consuming your endpoint.

Prerequisites

There are different approaches in in versioning your endpoints and you can check some of them in Advanced versioning in ASP.NET Core Web API article. My favorite is the route based versioning, for the simple reason which is that consuming version can be easily determined from the URL client is using to execute HTTP method against. Other methods are equally useful and effective, it;s just my personal opinion.

In this article I am going to use Swagger to document and describe versioned endpoint of the ASP.NET Core WebAPI service and the service will use route based versioning. So lets start with our demo project and first add Nuget package references.

  <ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore.App" />
    <PackageReference Include="Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer" Version="2.2.0" />
    <PackageReference Include="Microsoft.AspNetCore.Mvc.Versioning" Version="2.3.0" />
    <PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="2.1.1" />
    <PackageReference Include="Swashbuckle.AspNetCore" Version="3.0.0" />
    <PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="3.0.0" />
  </ItemGroup>
    

Before we start coding and adding Swagger to the dependency injection and the pipeline of WebAPI application, we need to add some project settings thet will ensure Swagger can pick up our generated XML documentetion from the build result and some basic project meta data.

Note

in .NET Core AssemblyInfo.cs is not part of the project template anymore, so if you want t use it, you need to add it manually. Othrewise, you can add your assembly metadata to the project file directly and read them pretty much the same way you would read them from AssemblyInfo.cs class

  <PropertyGroup>
    <TargetFramework>netcoreapp2.1</TargetFramework>
    <DocumentationFile>bin\$(Configuration)\$(TargetFramework)\$(MSBuildThisFileName).xml</DocumentationFile>
    <PackageId>Sample.Versioning.Swagger</PackageId>
    <Product>Sample versioning service</Product>
    <Description>Sample product catalog REST API service project</Description>
  </PropertyGroup>
    

We are going to use this data to build description in our Swagger instance and show them to the user.

Project structure

Now let's see how are we going to organize out API versions. Since versioning and changes may reflect not only controllers, but our models or DTOs as well, we are going to contain all of them in a separate folder per version.

The project structure should look something like this

Swaggerversionapi

We are bounding our whole request context to the version, so you can modify both controllers and models independent in their own version namespace keeping you current version intact from the changes you might be doing on your new API version.

For the demo in this article I will use simple POCO class as a model and the change for the new version will consist in adding additional field in the model.

using System;
namespace Sample.Versioning.Swagger.V1.Models
{
/// <summary>
/// Sample product model /// </summary>
public class ProductModel
{
/// <summary>
/// Unique identifier of the product
/// </summary>
public Guid Id { get; set; }
/// <summary>
/// Descriptive name of the product
/// </summary>
public String Name { get; set; }
        /// <summary>
        /// Product price
        /// </summary>
        public double Price { get; set; }
    }
}

    

The version of the controller has to be also set by the ApiVersion attribute so that route gets handled properly.

using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Sample.Versioning.Swagger.V1.Models;

namespace Sample.Versioning.Swagger.V1.Controllers
{
    /// <summary>
    /// Sample versioning REST API
    /// </summary>
    [ApiVersion("1.0")]
    [Route("api/v{version:apiVersion}/[controller]")]
    [ApiController]
    public class ProductsController : ControllerBase
    {
        /// <summary>
        /// Retrieve all the products
        /// </summary>
        /// <returns>Collection of ProductModel instances</returns>
        [HttpGet]
        public async Task<IEnumerable<ProductModel>> Get()
        {
            return await Task.FromResult<IEnumerable<ProductModel>>(new List<ProductModel>()
            {
                new ProductModel()
                {
                    Id= Guid.Parse("6fab57fb-0c61-4552-9490-9161c2466e62"),
                    Name = "Product 1",
                    Price = 2.3
                },
                new ProductModel()
                {
                    Id= Guid.Parse("6648eb0f-0e54-4f6a-93a1-2825e3c8fc9d"),
                    Name = "Product 2",
                    Price = 3.4
                }
            }.ToArray());
        }
    }
}
    

Swagger wire up

Now when we have our project structure in place and we have the strategy to organize our versions, we can start integrating Swagger to our project. We already have the references added to the project, so the first stem is to add Swagger to our dependency injection in the Startup.cs

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {

            services.AddMvc();

            services.AddMvcCore()
                .AddJsonFormatters()
                .AddVersionedApiExplorer(
                      options =>
                      {
                          //The format of the version added to the route URL
                          options.GroupNameFormat = "'v'VVV";
                          //Tells swagger to replace the version in the controller route
                          options.SubstituteApiVersionInUrl = true;
                      }); ;

            services.AddApiVersioning(options => options.ReportApiVersions = true);
            services.AddSwaggerGen(
                options =>
                {
                    // Resolve the temprary IApiVersionDescriptionProvider service
                    var provider = services.BuildServiceProvider().GetRequiredService<IApiVersionDescriptionProvider>();

                    // Add a swagger document for each discovered API version
                    foreach (var description in provider.ApiVersionDescriptions)
                    {
                        options.SwaggerDoc(description.GroupName, new Info()
                        {
                            Title = $"{this.GetType().Assembly.GetCustomAttribute<System.Reflection.AssemblyProductAttribute>().Product} {description.ApiVersion}",
                            Version = description.ApiVersion.ToString(),
                            Description = description.IsDeprecated ? $"{this.GetType().Assembly.GetCustomAttribute<AssemblyDescriptionAttribute>().Description} - DEPRECATED" : this.GetType().Assembly.GetCustomAttribute<AssemblyDescriptionAttribute>().Description,

                        });
                    }

                    // Add a custom filter for settint the default values
                    options.OperationFilter<SwaggerDefaultValues>();

                    // Tells swagger to pick up the output XML document file
                    options.IncludeXmlComments(Path.Combine( 
                        Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), $"{this.GetType().Assembly.GetName().Name}.xml"
                        ));

                });

        }
    

First thing that is handled in the startup dependency injection configuration is route. For this we are using API Explorer Option to describe the way we want our route to be generated according to the verion of the endpoint. More information about the version formatting you can find in Version Format page on ASP.NET versioning repository page

Remeber we added some meta to our ptoject? Well now it's time to use them. We will read our project meatadata and display it as a documentation on our Swagger instance endpoint.

You probably noticed that we are referencing SwaggerDefaultValues class in our code. This is a custom implementation of IOperationFilter interface and it is used to set document the API version by Swagger.

using Swashbuckle.AspNetCore.Swagger;
using Swashbuckle.AspNetCore.SwaggerGen;
using System.Linq;

namespace Sample.Versioning.Swagger
{
    /// <summary>
    /// Represents the Swagger/Swashbuckle operation filter used to document the implicit API version parameter.
    /// </summary>
    /// <remarks>This <see cref="IOperationFilter"/> is only required due to bugs in the <see cref="SwaggerGenerator"/>.
    /// Once they are fixed and published, this class can be removed.</remarks>
    public class SwaggerDefaultValues : IOperationFilter
    {
        /// <summary>
        /// Applies the filter to the specified operation using the given context.
        /// </summary>
        /// <param name="operation">The operation to apply the filter to.</param>
        /// <param name="context">The current operation filter context.</param>
        public void Apply(Operation operation, OperationFilterContext context)
        {
            if (operation.Parameters == null)
            {
                return;
            }
            foreach (var parameter in operation.Parameters.OfType<NonBodyParameter>())
            {
                var description = context.ApiDescription.ParameterDescriptions.First(p => p.Name == parameter.Name);
                var routeInfo = description.RouteInfo;

                if (parameter.Description == null)
                {
                    parameter.Description = description.ModelMetadata?.Description;
                }

                if (routeInfo == null)
                {
                    continue;
                }

                if (parameter.Default == null)
                {
                    parameter.Default = routeInfo.DefaultValue;
                }

                parameter.Required |= !routeInfo.IsOptional;
            }
        }
    }
}

    

Once we have all setup in the dependency injection services, we need to add Swagger to the pipeline, so that our requests to Swagger route get handled properly. We do this by adding Swagger and SwaggerUI in the Configure method in Startup.cs class

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IHostingEnvironment env, IApiVersionDescriptionProvider provider)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.UseSwagger();
            app.UseSwaggerUI(
                options =>
                {
                    //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());
                    }
                });

            app.UseMvc();
        }
    

Now everything is ready for the single version test, but before we hit F5, let configure our startup in launchSettings.json file to target Swagger route on the startup of the application

{
  "$schema": "http://json.schemastore.org/launchsettings.json",
  "iisSettings": {
    "windowsAuthentication": false, 
    "anonymousAuthentication": true, 
    "iisExpress": {
      "applicationUrl": "http://localhost:51009",
      "sslPort": 0
    }
  },
  "profiles": {
    "IIS Express": {
      "commandName": "IISExpress",
      "launchBrowser": true,
      "launchUrl": "swagger",
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      }
    },
    "Sample.Versioning.Swagger": {
      "commandName": "Project",
      "launchBrowser": true,
      "launchUrl": "swagger",
      "applicationUrl": "http://localhost:5000",
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      }
    }
  }
}
    

We are ready to test the single version Swagger endpoint

Swaggerversionapi V1

You can see that Swagger managed to pick up the model XML description added to the class and all the AssemblyInfo information picked up from thr project file. Now let's add a minor update on the model to test our new version of the API endpoint.

As we have isolated version folder V1 for our version 1.0, we'll add new folder for version 2.0 with name V2 where we are going to store our new version controller and model. The change we are going to implement in a new version is Description field added to the model

using System;

namespace Sample.Versioning.Swagger.V2.Models
{
    /// <summary>
    /// Sample product model 
    /// </summary>
    public class ProductModel
    {
        /// <summary>
        /// Unique identifier of the product
        /// </summary>
        public Guid Id { get; set; }

        /// <summary>
        /// Descriptive name of the product
        /// </summary>
        public String Name { get; set; }

        /// <summary>
        /// Product price
        /// </summary>
        public double Price { get; set; }

        /// <summary>
        /// Product brief description
        /// </summary>
        public String Description { get; set; }
    }
}

    

Controller for V2 is pretty much the same with difference of referencing the new model namespace V2 instead of V1

using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Sample.Versioning.Swagger.V2.Models;

namespace Sample.Versioning.Swagger.V2.Controllers
{
    /// <summary>
    /// Sample versioning REST API
    /// </summary>
    [ApiVersion("2.0")]
    [Route("api/v{version:apiVersion}/[controller]")]
    [ApiController]
    public class ProductsController : ControllerBase
    {
        /// <summary>
        /// Retrieve all the products
        /// </summary>
        /// <returns>Collection of ProductModel instances</returns>
        [HttpGet]
        public async Task<IEnumerable<ProductModel>> Get()
        {
            return await Task.FromResult<IEnumerable<ProductModel>>(new List<ProductModel>()
            {
                new ProductModel()
                {
                    Id= Guid.Parse("6fab57fb-0c61-4552-9490-9161c2466e62"),
                    Name = "Product 1",
                    Price = 2.3,
                    Description = "This is product 1 descritpion"
                },
                new ProductModel()
                {
                    Id= Guid.Parse("6648eb0f-0e54-4f6a-93a1-2825e3c8fc9d"),
                    Name = "Product 2",
                    Price = 3.4,
                    Description = "This is product 2 descritpion"
                }
            }.ToArray());
        }
    }
}
    

And now when we run the API project we'll see two option in the spec list in top right corner with our new version V2. Once we swith to it, we'll have our new endpoint description with our new description filed in the model

Swaggerversionapi V2

To make thing easier, I uploaded the whole sample project to Github you can go directly to the repository and look at the code of this sample project yourself of clone the whole repository to your PC https://github.com/dejanstojanovic/api-versioning-swagger

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 includion 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