Integrating ASP.NET Core API Versioning with Swagger UI

In this article, let's see how we can dynamically integrate Swagger UI documentation, for all the API versions available in our ASP.NET Core Web API.

Introduction

In a previous article, we have looked into how ASP.NET Core has built-in support for API versioning and how we can implement API versioning using the Microsoft.AspNetCore.Mvc.Versioning package, in detail with an illustrating example. You can find the complete article here.

The result is the creation of two API versions of HeroesController; one mapped for the path /api/v1/heroes – which also maps to the conventional /api/heroes API (for support to older API routes), and a newer /api/v2/heroes which adds some new endpoints apart from modified versions of existing V1 API.

The controllers which represent the two versions of the API endpoint are as below:

## /api/v1/heroes ##
namespace SwaggerHeroes.Api.Controllers.V1
{
    [ApiController]
    [ApiVersion("1.0")]
    [Route("api/v{version:apiVersion}/[controller]")]
    // [Route("api/[controller]")] // for backward compatibility
    public class HeroesController : ControllerBase
    {
        private readonly ILogger<HeroesController> _logger;
        private readonly IHeroesRepository _data;

        public HeroesController(
            ILogger<HeroesController> logger, IDataService data)
        {
            _logger = logger;
            _data = data.Heroes;
        }

        [MapToApiVersion("1.0")]
        [HttpGet, Route("alive")]
        public string Alive()
        {
            return "Captain, 1.0 Here. I'm Alive and Kicking!";
        }

        [MapToApiVersion("1.0")]
        [HttpGet]
        public IEnumerable<Hero> Get()
        {
            return _data.All();
        }

        [MapToApiVersion("1.0")]
        [HttpGet, Route("{id}")]
        public Hero Get(int id)
        {
            return _data.Single(x => x.Id == id);
        }

        [MapToApiVersion("1.0")]
        [HttpPost]
        public Hero Post(Hero model)
        {
            var item = new Hero
            {
                Category = model.Category,
                HasCape = model.HasCape,
                IsAlive = model.IsAlive,
                Name = model.Name,
                Powers = model.Powers
            };
            return _data.Create(item);
        }

        [MapToApiVersion("1.0")]
        [HttpGet, Route("searchbyname")]
        public IEnumerable<Hero> SearchByName(string name = "")
        {
            return _data.Search(x => x.Name.Contains(name));
        }
    }
}

## /api/v2/heroes ##
namespace SwaggerHeroes.Controllers.V2
{
    [ApiController]
    [ApiVersion("2.0")]
    [Route("api/v{version:apiVersion}/[controller]")]
    public class HeroesController : ControllerBase
    {
        private readonly ILogger<HeroesController> _logger;
        private readonly IHeroesRepository _data;

        public HeroesController(
            ILogger<HeroesController> logger, IDataService data)
        {
            _logger = logger;
            _data = data.Heroes;
        }

        
        [MapToApiVersion("2.0")]
        [HttpGet, Route("alive")]
        public string Alive()
        {
            return "Captain, 2.0 Here. I'm Alive and Kicking!";
        }

        [MapToApiVersion("2.0")]
        [HttpGet]
        public IEnumerable<Hero> Get()
        {
            return _data.All();
        }

        [MapToApiVersion("2.0")]
        [HttpPost]
        public Hero Post(HeroItem model)
        {
            var item = new Hero
            {
                Category = model.Category,
                HasCape = model.HasCape,
                IsAlive = model.IsAlive,
                Name = model.Name,
                Powers = model.Powers
            };
            return _data.Create(item);
        }

        [MapToApiVersion("2.0")]
        [HttpGet, Route("searchbyname")]
        public IEnumerable<Hero> SearchByName(string name = "")
        {
            return _data.Search(x => x.Name.Contains(name));
        }

        [MapToApiVersion("2.0")]
        [HttpGet, Route("categories")]
        public IEnumerable<string> Categories()
        {
            return (string[])Enum.GetNames(typeof(Category));
        }
    }
}

When we accessed the APIs, the responses for the /alive endpoint in both the versions are as below:

GET 'https://localhost:5001/api/v1/heroes/alive' 
Captain, 1.0 Here. I'm Alive and Kicking!
GET 'https://localhost:5001/api/v2/heroes/alive' 
Captain, 2.0 Here. I'm Alive and Kicking

The Swagger Integration Problem

Let’s add a swagger documentation for the API solution that now we have created. Let’s begin by installing the Swashbuckle.AspNetCore nuget and then adding the required services and middleware.

The startup class looks like below:

namespace Heroes.Api
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddScoped<IHeroesRepository, HeroesRepository>();
            services.AddControllers();

            services.AddApiVersioning(setup =>
            {
                setup.DefaultApiVersion = new ApiVersion(1, 0);
                setup.AssumeDefaultVersionWhenUnspecified = true;
                setup.ReportApiVersions = true;
            });

            services.AddSwaggerGen();
        }

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

            app.UseHttpsRedirection();

            app.UseSwagger();
            app.UseSwaggerUI(options =>
            {
                options.SwaggerEndpoint("/swagger/v1/swagger.json", "V1");
            });

            app.UseRouting();

            app.UseAuthorization();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllers();
            });
        }
    }
}

When we accessing the swagger UI page for the APIs, we see an error message on the UI and an exception in the console.

An unhandled exception has occurred while executing the request.
Swashbuckle.AspNetCore.SwaggerGen.SwaggerGeneratorException: Conflicting method/path combination "GET api/v{version}/Heroes/alive" for actions - Heroes.Api.Controllers.V2.HeroesController.Alive (Heroes.Api),Heroes.Api.Controllers.V1.HeroesController.Alive (Heroes.Api). Actions require a unique method/path combination for Swagger/OpenAPI 3.0.

Why Swagger isn’t working?

From the error, we can understand that the API Versioning setup we’ve put forth is not picked up by the Swagger while parsing the APIs for their metadata. Swagger is still treating both the API controllers under a similar path and hence throwing up an exception that there are multiple actions available for the same path.

Solution – Mapping to API Versions

To solve this, let’s begin by marking the endpoints with the information on which version to map to. To do this, we decorate every endpoint with the [MapToApiVersion(“”)] attribute, which takes one string argument – the version which this endpoint to be mapped against.

For example, our V2 HeroesController becomes like this:

namespace Heroes.Api.Controllers.V2
{
    [ApiController]
    [ApiVersion("2.0")]
    [Route("api/v{version:apiVersion}/[controller]")]
    public class HeroesController : ControllerBase
    {
        public HeroesController(
            ILogger<HeroesController> logger, 
            IHeroesRepository data)
        {
            // code ommitted for brevity
        }
        
        [MapToApiVersion("2.0")]
        [HttpGet, Route("alive")]
        public string Alive()
        {
            // code ommitted for brevity
        }

        [MapToApiVersion("2.0")]
        [HttpGet]
        public IEnumerable<Hero> Get()
        {
            // code ommitted for brevity
        }

        [MapToApiVersion("2.0")]
        [HttpPost]
        public Hero Post(HeroItem model)
        {
            // code ommitted for brevity
        }

        [MapToApiVersion("2.0")]
        [HttpGet, Route("searchbyname")]
        public IEnumerable<Hero> SearchByName(string name = "")
        {
            // code ommitted for brevity
        }

        [MapToApiVersion("2.0")]
        [HttpGet, Route("categories")]
        public IEnumerable<string> Categories()
        {
            // code ommitted for brevity
        }
    }
}

How to fetch Version Information with API Explorer

To get the information on these versions and endpoints, we add the Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer nuget package which provides the metadata for the APIs based on how they are decorated. In our case, it returns us the Version information of each action.

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

Next, add the ApiExplorer service to the collection.

public void ConfigureServices(IServiceCollection services)
{
    services.AddScoped<IHeroesRepository, HeroesRepository>();
    services.AddControllers();

    services.AddApiVersioning(setup =>
    {
        setup.DefaultApiVersion = new ApiVersion(1, 0);
        setup.AssumeDefaultVersionWhenUnspecified = true;
        setup.ReportApiVersions = true;
    });

    services.AddVersionedApiExplorer(setup =>
    {
        setup.GroupNameFormat = "'v'VVV";
        setup.SubstituteApiVersionInUrl = true;
    });

    services.AddSwaggerGen();
}

And we inject the ApiExplorer service into the Configure method and use the metadata it has collected into the SwaggerUI middleware.

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

    app.UseHttpsRedirection();

    app.UseSwagger();

    app.UseSwaggerUI(options =>
    {
        foreach (var description in provider.ApiVersionDescriptions)
        {
            options.SwaggerEndpoint(
 
quot;/swagger/{description.GroupName}/swagger.json", description.GroupName.ToUpperInvariant()); } }); app.UseRouting(); app.UseAuthorization(); app.UseEndpoints(endpoints => { endpoints.MapControllers(); }); } 

Why Swagger doesn’t pickup all the Versions?

The result of this fix – the error disappears and we are now able to see two Versions in the Swagger version dropdown – the V1 works as well!

But when you try look into V2 version, you see a not found error. And actually the swagger json is not generated for V2 APIs (/swagger/v2/swagger.json).

How do we solve this?

Configuring SwaggerOptions to pickup all the Versions

We need to ensure that Swagger creates documentation for all the available versions provided by the ApiExplorer. For a single version, we can create a documentation info inside the SwaggerGen() service. But when we have multiple versions, its not a good way to hardcode documentation info.

Instead, we configure the SwaggerGenOptions by giving it a NamedOptions implementation to substitute it during it runtime.

It is because to get the information on the APIs we need to use the ApiExplorer service which we can’t inject it inside the ConfigureServices (not a good practice). So this is the way.

The ConfigureSwaggerOptions class looks like below:

namespace Heroes.Api
{
    public class ConfigureSwaggerOptions
        : IConfigureNamedOptions<SwaggerGenOptions>
    {
        private readonly IApiVersionDescriptionProvider provider;

        public ConfigureSwaggerOptions(
            IApiVersionDescriptionProvider provider)
        {
            this.provider = provider;
        }

        public void Configure(SwaggerGenOptions options)
        {
            // add swagger document for every API version discovered
            foreach (var description in provider.ApiVersionDescriptions)
            {
                options.SwaggerDoc(
                    description.GroupName, 
                    CreateVersionInfo(description));
            }
        }

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

        private OpenApiInfo CreateVersionInfo(
                ApiVersionDescription description)
        {
            var info = new OpenApiInfo()
            {
                Title = "Heroes API",
                Version = description.ApiVersion.ToString()
            };

            if (description.IsDeprecated)
            {
                info.Description += " This API version has been deprecated.";
            }

            return info;
        }
    }
}

we wire up the Options to the service collection.

public void ConfigureServices(IServiceCollection services)
{
    services.AddScoped<IHeroesRepository, HeroesRepository>();
    services.AddControllers();

    services.AddApiVersioning(setup =>
    {
        setup.DefaultApiVersion = new ApiVersion(1, 0);
        setup.AssumeDefaultVersionWhenUnspecified = true;
        setup.ReportApiVersions = true;
    });

    services.AddVersionedApiExplorer(setup =>
    {
        setup.GroupNameFormat = "'v'VVV";
        setup.SubstituteApiVersionInUrl = true;
    });

    services.AddSwaggerGen();
    services.ConfigureOptions<ConfigureSwaggerOptions>();
}

When we try loading the Swagger now, we get both the versions running, with Information created for both the versions.

Buy Me A Coffee

Found this article helpful? Please consider supporting!

Conclusion

Swagger API documentation works well with the AspNetCore APIs, but when we create versions of APIs with similar routes/methods things get a little rusty. With help from the ApiExplorer nuget and a little tweak on how the Info is generated, we get this setup running without any issues.

We can also extend this setup a bit by ensuring proper default values are loaded for the API documentation with the help of IOperationFilter implementations, but for a quickstart setup this would suffice.

The code snippets used in this article are a part of a solution called SwaggerHeroes.

It is a simple boilerplate solution, that demonstrates the following key takeaways:

  1. Swagger Integration
  2. API Versioning
  3. Swagger UI for API Versions

It is free and opensource.

Please do leave a Star if you find it useful. You can find the repository here – SwaggerHeroes repository. You may want to check out some other articles on Swagger –


Buy Me A Coffee

Found this article helpful? Please consider supporting!

Ram
Ram

I'm a full-stack developer and a software enthusiast who likes to play around with cloud and tech stack out of curiosity. You can connect with me on Medium, Twitter or LinkedIn.

7 Comments

  1. Hello. Thank you for the clear description.
    One question: Is there a way to parameterize the ‘Title’ field when creating the OpenApiInfo instances, instead of having it hard-coded?
    Thanks.

    • Hello there! That’s an interesting use-case. We can actually maintain these spec information inside the appsettings JSON file and can read them via Configuration. I’ve tried building such a solution using IConfiguration and Options pattern and it worked like a charm! Here’s the PR that has the changes – https://github.com/referbruv/SwaggerHeroes/pull/2

      I hope this works for you.

  2. HI,
    Thanks for your post, it’s been very helpful.
    The only thing I couldn’t solve is that it’s not showing the available versions. Only V1 appears as an option.
    I created V1 and V2 and saw that in the Foreach routine, the system finds these two options. I’m using Net 6.0
    Can you help?

    • Issue resolved,
      I haven’t seen github

      thanks

      app.UseSwaggerUI(
      options =>
      {
      foreach (var description in provider.ApiVersionDescriptions)
      {
      options.SwaggerEndpoint($”../swagger/{description.GroupName}/swagger.json”, description.GroupName.ToUpperInvariant());
      }
      });

Leave a Reply

Your email address will not be published. Required fields are marked *