Introduction
In this post, we’ll talk through how we can add API versions to the Swagger page using ASP.NET 6.0. This will allow users to go to the Swagger page and they’ll have a drop down with the different API versions and they can then view the docs for those particular versions. ASP.NET doesn’t do this by default so you have to configure it yourself but no problem, I can show you how.
Why use API versions?
Okay, so this is quite cool but why would we do this? Basically you may have to make a breaking change but have many consumers of the API. Ideally you want to avoid breaking changes but sometimes it’s not avoidable and you cannot update all the clients at the same time. So, using API versions allows you to have two versions running side by side and then allow the clients to migrate over to the new versions when they can.
Add API versions in Swagger
Follow these below steps to add API versions into your Swagger page.
- Install required NuGet packages into your API project:
- Microsoft.AspnetCore.Mvc.Versioning
- Microsoft.AspnetCore.Mvc.Versioning.ApiExplorer
2. Create a new class called ConfigureSwaggerGenOptions
as below.
This class has a Configure()
method which loops through all the API versions and adds a new Swagger doc for each version. Make sure you change the Title
property on line 28.
using Microsoft.AspNetCore.Mvc.ApiExplorer;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;
namespace LSE.Stocks.Api.Swagger
{
public class ConfigureSwaggerGenOptions : IConfigureOptions<SwaggerGenOptions>
{
private readonly IApiVersionDescriptionProvider _apiVersionDescriptionProvider;
public ConfigureSwaggerGenOptions(IApiVersionDescriptionProvider apiVersionDescriptionProvider)
=> _apiVersionDescriptionProvider = apiVersionDescriptionProvider;
public void Configure(SwaggerGenOptions options)
{
foreach (var description in _apiVersionDescriptionProvider.ApiVersionDescriptions)
{
options.SwaggerDoc(description.GroupName, CreateOpenApiInfo(description));
}
}
private static OpenApiInfo CreateOpenApiInfo(ApiVersionDescription description)
{
var info = new OpenApiInfo()
{
Title = "LSE.Stocks.API",
Version = description.ApiVersion.ToString()
};
if (description.IsDeprecated)
{
info.Description += " (deprecated)";
}
return info;
}
}
}
3. Create a new class called SwaggerDefaultValues
as below.
This class implements IOperationFilter
which basically performs some transformations on the operation by setting the Description
and Schema
properties for each of the parameters within the API operation.
using Microsoft.AspNetCore.Mvc.ApiExplorer;
using Microsoft.OpenApi.Any;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;
namespace LSE.Stocks.Api.Swagger;
public class SwaggerDefaultValues : IOperationFilter
{
public void Apply(OpenApiOperation operation, OperationFilterContext context)
{
var apiDescription = context.ApiDescription;
operation.Deprecated |= apiDescription.IsDeprecated();
if (operation.Parameters == null)
{
return;
}
foreach (var parameter in operation.Parameters)
{
var description = apiDescription.ParameterDescriptions.First(p => p.Name == parameter.Name);
if (parameter.Description is null)
{
parameter.Description = description.ModelMetadata?.Description;
}
if (parameter.Schema.Default is null && description.DefaultValue is not null)
{
parameter.Schema.Default = new OpenApiString(description.DefaultValue.ToString());
}
parameter.Required |= description.IsRequired;
}
}
}
4. Add Swagger Operation Filter and API Versioning to ConfigureServices()
in Startup.cs
You basically need to add the below to the ConfigureServices()
method in Startup.cs
. This configures the Swagger Gen service by using the SwaggerDefaultValues
class we created earlier and also adds API versioning to ASP.NET. You’ll note that it’ll assume the default version when unspecified and that the default is set to 1.0.
services.AddApiVersioning(o =>
{
o.AssumeDefaultVersionWhenUnspecified = true;
o.DefaultApiVersion = new ApiVersion(1, 0);
});
services.AddVersionedApiExplorer(o =>
{
o.GroupNameFormat = "'v'VVV";
o.SubstituteApiVersionInUrl = true;
});
services.AddSwaggerGen(o =>
{
o.OperationFilter<SwaggerDefaultValues>();
});
services.AddTransient<IConfigureOptions<SwaggerGenOptions>, ConfigureSwaggerOptions>();
5. Create a Swagger endpoint for each API version
Here we configure the Swagger UI by adding an endpoint for each of the API versions based on the ApiVersionDescriptions
. Add the below code to your Configure()
method in Startup.cs
. Make sure to rename your API from MyApi to the name of your API.
app.UseSwagger();
app.UseSwaggerUI(o =>
{
foreach (var description in versionDescriptionProvider.ApiVersionDescriptions)
{
o.SwaggerEndpoint($"/swagger/{description.GroupName}/swagger.json",
$"MyApi - {description.GroupName.ToUpper()}");
}
});
Your full startup.cs
should look something like below. See line 27 which calls the ConfigureSwaggerGenOptions
method on line 61 which is where we’re addingSwaggerDefaultValues
as an operation filter for Swagger. Also, see line 34 which calls the AddApiVersioning()
method on line 73 which configures the API versions in ASP.NET.
using LSE.Stocks.Api.Middleware;
using LSE.Stocks.Api.ServiceCollectionExtensions;
using LSE.Stocks.Api.Swagger;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ApiExplorer;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Options;
using Swashbuckle.AspNetCore.SwaggerGen;
using System.IO;
using System.Reflection;
namespace LSE.Stocks.Api;
public class Startup
{
public IConfiguration Configuration { get; }
public Startup(IConfiguration configuration) => Configuration = configuration;
public void ConfigureServices(IServiceCollection services)
{
services.AddEndpointsApiExplorer()
.AddSwaggerGen(o => ConfigureSwaggerGenOptions(o))
.AddMediatorServices()
.AddApplicationInsightsTelemetry()
.AddRepositories(Configuration)
.AddTransient<IConfigureOptions<SwaggerGenOptions>, ConfigureSwaggerGenOptions>()
.AddControllers();
AddApiVersioning(services);
}
public static void Configure(IApplicationBuilder app, IWebHostEnvironment env,
IApiVersionDescriptionProvider apiVersionDescriptionProvider)
{
if (env.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI(o =>
{
foreach (var description in apiVersionDescriptionProvider.ApiVersionDescriptions)
{
o.SwaggerEndpoint($"/swagger/{description.GroupName}/swagger.json",
$"LSE.Stocks.API - {description.GroupName.ToUpper()}");
}
});
}
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthorization();
app.UseCustomExceptionHandler();
app.UseEndpoints(endpoints => endpoints.MapControllers());
}
private static void ConfigureSwaggerGenOptions(SwaggerGenOptions o)
{
AddSwaggerXmlComments(o);
o.OperationFilter<SwaggerDefaultValues>();
}
private static void AddSwaggerXmlComments(SwaggerGenOptions o)
{
var xmlFilename = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
o.IncludeXmlComments(Path.Combine(AppContext.BaseDirectory, xmlFilename));
}
private static void AddApiVersioning(IServiceCollection services)
{
services.AddApiVersioning(o =>
{
o.AssumeDefaultVersionWhenUnspecified = true;
o.DefaultApiVersion = new ApiVersion(1, 0);
});
services.AddVersionedApiExplorer(o =>
{
o.GroupNameFormat = "'v'VVV";
o.SubstituteApiVersionInUrl = true;
});
}
}
You’ll see that I’m using XML comments and adding them to Swagger. If you want to know more about how this is done, see here.
6. Update your controllers to use route based API versioning
Here we’re going to use route-based API versioning which is where you’d add the version number into the URL used by clients e.g. https://api.adventure-works.com/v1/shareprice
. To do this, we update the Route
attribute on the controller. This one below is going to configure the controller to be triggered on https://api.adventure-works.com/v1/shareprice
and https://api.adventure-works.com/shareprice
.
using LSE.Stocks.Api.Models;
using LSE.Stocks.Application.Services.Shares.Queries.GetSharePrice;
using LSE.Stocks.Domain.Models.Shares;
using MediatR;
using Microsoft.AspNetCore.Mvc;
namespace LSE.Stocks.Api.Controllers.V1;
[ApiVersion("1.0")]
[Route("SharePrice")]
[Route("v{version:apiVersion}/SharePrice")]
[ApiController]
public class SharePriceController : Controller
{
private readonly IMediator _mediator;
public SharePriceController(IMediator mediator) => _mediator = mediator;
/// <summary>
/// Gets the price for a ticker symbol
/// </summary>
/// <param name="tickerSymbol"></param>
/// <returns>A SharePriceResponse which contains the price of the share</returns>
/// <response code="200">Returns 200 and the share price</response>
/// <response code="400">Returns 400 if the query is invalid</response>
[HttpGet]
public async Task<ActionResult<SharePriceResponse>> GetPrice([FromQuery] string tickerSymbol)
{
var sharePriceQueryResponse = await _mediator.Send(new GetSharePriceQuery(tickerSymbol));
return new OkObjectResult(BuildSharePriceQueryResponse(sharePriceQueryResponse.SharePrice));
}
private static SharePriceResponse BuildSharePriceQueryResponse(SharePrice sharePrice)
=> new(sharePrice.TickerSymbol, sharePrice.Price);
}
This controller below is configured to work with both API version 1.0 and 2.0 (see that it has two ApiVersion
attributes on it). It’ll work with https://api.adventure-works/trade
, https://api.adventure-works/v1/trade
and https://api.adventure-works/v2/trade
.
using LSE.Stocks.Api.Models;
using LSE.Stocks.Application.Services.Shares.Commands.SaveTrade;
using MediatR;
using Microsoft.AspNetCore.Mvc;
namespace LSE.Stocks.Api.Controllers.Common;
[ApiVersion("1.0")]
[ApiVersion("2.0")]
[Route("[controller]")]
[Route("v{version:apiVersion}/[controller]")]
[ApiController]
public class TradeController : Controller
{
private readonly IMediator _mediator;
public TradeController(IMediator mediator) => _mediator = mediator;
/// <summary>
/// Saves a trade of a share
/// </summary>
/// <param name="tradeRequest"></param>
/// <returns>Ok</returns>
/// <response code="200">Returns 200 if the trade was saved successfully</response>
/// <response code="400">Returns 400 if the request to save a trade was invalid</response>
[HttpPost]
public async Task<ActionResult> SaveTrade([FromBody] SaveTradeRequest tradeRequest)
{
await _mediator.Send(new SaveTradeCommand(tradeRequest.TickerSymbol, tradeRequest.Price,
tradeRequest.Count, tradeRequest.BrokerId));
return Ok();
}
}
Multiple API versions in Swagger
Now you’ve done the above, you should be able to load up the Swagger page and see a version selector in the top right:
Here’s the v2 version. You’ll note that the SharePrice GET operation no longer takes a query string parameter and now requires the tickerSymbol
to be in the URL (hence the breaking change):
Conclusion
So, there you have it – multiple API versions displayed in Swagger. We’ve done this by adding versions to our controllers then configuring ASP.NET with these versions and then configuring Swagger to create a doc and UI for each of the versions. Hopefully this makes working with multiple versions a bit easier. You’ll find the full code project here on my GitHub.
Happy Coding!