What every ASP.NET Core Web API project needs - Part 2 - API versioning and Swagger
Mohsen Esmailpour
Posted on March 1, 2021
In my previous article, I wrote about adding Serilog to the project and configuring it through the appsettings.json
file. In this article, I'm going to add Swagger for API documentation and API versioning as well. All codes that I'm going to implement, will be added to the project I've created in the previous article.
Let's get started. As you might know, there are several ways to versioning API, by URL, HTTP header, etc. We are going to add API versioning by URL.
Step 1 - Install package
Open the cool-webpi project and Install Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer package
Step 2 - Add versioning configuration
Open the Startup.cs
file and add the following configuration to the ConfigureServices
method:
services.AddApiVersioning(options =>
{
// reporting api versions will return the headers "api-supported-versions" and "api-deprecated-versions"
options.ReportApiVersions = true;
});
services.AddVersionedApiExplorer(options =>
{
// add the versioned api explorer, which also adds IApiVersionDescriptionProvider service
// note: the specified format code will format the version as "'v'major[.minor][-status]"
options.GroupNameFormat = "'v'VVV";
// note: this option is only necessary when versioning by url segment. the SubstitutionFormat
// can also be used to control the format of the API version in route templates
options.SubstituteApiVersionInUrl = true;
});
Now run the application and you'll get noticed that api-version
input added to each API document. Now call WeatherForcast
API without providing any value for api-version
input:
You get an error indicates that the API version is required. Enter value 1
into the api-version
input and call API again and you get the result.
Step 3 - Add versioning to APIs
Create a new folder at the project root and name it Apis
. Add two more folders to the Apis
folder, V1
and V2
. Move Controllers
folder to Apis\V1
folder:
Open WeatherForecastController
file and add the ApiVersion
attribute and modify the Route
attribute value:
[ApiController]
[ApiVersion("1.0")]
[Route("api/v{version:apiVersion}/[controller]")]
public class WeatherForecastController : ControllerBase
{
...
Again run the application and you see that api-version
input no longer exists:
Now duplicate WeatherForecastController
in V2 folder:
[ApiController]
[ApiVersion("2.0")]
[Route("api/v{version:apiVersion}/[controller]")]
public class WeatherForecast2Controller : ControllerBase
{
...
Now we have 2 versions of WeatherForecast API, nevertheless, we are not able to find Swagger documentation for V2:
Check out the official API versioning Github repository to find out more information.
Step 4 - Add versioning to Swagger
When you create an ASP.NET Core Web API project, Swagger is installed by default unless you uncheck this tick:
We are going to change the default configuration of Swagger.
- Update
Swashbuckle.AspNetCore
to the latest version (6 and above). - Create a new folder at the project root and name it
Infrastructure
and add another folderSwagger
to theInfrastructure
folder. - Add a new file
SwaggerDefaultValues.cs
toSwagger
folder and copy following codes:
/// <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(OpenApiOperation operation, OperationFilterContext context)
{
var apiDescription = context.ApiDescription;
operation.Deprecated |= apiDescription.IsDeprecated();
// REF: https://github.com/domaindrivendev/Swashbuckle.AspNetCore/issues/1752#issue-663991077
foreach (var responseType in context.ApiDescription.SupportedResponseTypes)
{
// REF: https://github.com/domaindrivendev/Swashbuckle.AspNetCore/blob/b7cf75e7905050305b115dd96640ddd6e74c7ac9/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/SwaggerGenerator.cs#L383-L387
var responseKey = responseType.IsDefaultResponse ? "default" : responseType.StatusCode.ToString();
var response = operation.Responses[responseKey];
foreach (var contentType in response.Content.Keys)
if (responseType.ApiResponseFormats.All(x => x.MediaType != contentType))
response.Content.Remove(contentType);
}
if (operation.Parameters == null)
return;
// REF: https://github.com/domaindrivendev/Swashbuckle.AspNetCore/issues/412
// REF: https://github.com/domaindrivendev/Swashbuckle.AspNetCore/pull/413
foreach (var parameter in operation.Parameters)
{
var description = apiDescription.ParameterDescriptions.First(p => p.Name == parameter.Name);
parameter.Description ??= description.ModelMetadata.Description;
if (parameter.Schema.Default == null && description.DefaultValue != null)
{
// REF: https://github.com/Microsoft/aspnet-api-versioning/issues/429#issuecomment-605402330
var json = JsonSerializer.Serialize(description.DefaultValue, description.ModelMetadata.ModelType);
parameter.Schema.Default = OpenApiAnyFactory.CreateFromJson(json);
}
parameter.Required |= description.IsRequired;
}
}
}
- Add another file
ConfigureSwaggerOptions.cs
toSwagger
folder and copy following codes:
/// <summary>
/// Configures the Swagger generation options.
/// </summary>
/// <remarks>This allows API versioning to define a Swagger document per API version after the
/// <see cref="IApiVersionDescriptionProvider"/> service has been resolved from the service container.</remarks>
public class ConfigureSwaggerOptions : IConfigureOptions<SwaggerGenOptions>
{
private readonly IApiVersionDescriptionProvider _provider;
/// <summary>
/// Initializes a new instance of the <see cref="ConfigureSwaggerOptions"/> class.
/// </summary>
/// <param name="provider">The <see cref="IApiVersionDescriptionProvider">provider</see> used to generate Swagger documents.</param>
public ConfigureSwaggerOptions(IApiVersionDescriptionProvider provider) => _provider = provider;
/// <inheritdoc />
public void Configure(SwaggerGenOptions options)
{
// add a swagger document for each discovered API version
// note: you might choose to skip or document deprecated API versions differently
foreach (var description in _provider.ApiVersionDescriptions)
options.SwaggerDoc(description.GroupName, CreateInfoForApiVersion(description));
}
private static OpenApiInfo CreateInfoForApiVersion(ApiVersionDescription description)
{
var info = new OpenApiInfo()
{
Title = "Cool Web API",
Version = description.ApiVersion.ToString(),
Description = "A Cool Web API Sample.",
Contact = new OpenApiContact { Name = "Mosi Esmailpour", Email = "mo.esmp@gmail.com" },
License = new OpenApiLicense { Name = "MIT", Url = new Uri("https://opensource.org/licenses/MIT") }
};
if (description.IsDeprecated)
info.Description += " This API version has been deprecated.";
return info;
}
}
- Open the
Startup.cs
file and inConfigureServices
method delete the default swagger configuration:
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo { Title = "CoolWebApi", Version = "v1" });
});
- Add the following configuration:
services.AddTransient<IConfigureOptions<SwaggerGenOptions>, ConfigureSwaggerOptions>();
services.AddSwaggerGen(options =>
{
// add a custom operation filter which sets default values
options.OperationFilter<SwaggerDefaultValues>();
});
- In
Configure
method addIApiVersionDescriptionProvider
parameter:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IApiVersionDescriptionProvider provider)
- In
Configure
method delete the Swagger UI default configuration:
app.UseSwagger();
app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "CoolWebApi v1"));
- Add the following codes to configure Swagger UI:
app.UseSwagger(options => { options.RouteTemplate = "api-docs/{documentName}/docs.json"; });
app.UseSwaggerUI(options =>
{
options.RoutePrefix = "api-docs";
foreach (var description in provider.ApiVersionDescriptions)
options.SwaggerEndpoint($"/api-docs/{description.GroupName}/docs.json", description.GroupName.ToUpperInvariant());
});
I've changed the default Swagger route prefix from swagger
to api-docs
. Right-click the project and select Properties
and in the Debug
tab change Launch browser
value to api-docs
(if don't like to change the default swagger route, skip this).
Now run the application and you can see both APIs documents:
Step 5 - Add XML comments to API documentation
Sometimes it would be helpful to add extra information to the APIs. To add XML comments:
- Right-click the project in Solution Explorer and select Edit
CoolWebApi.csproj
or double click on it - Add the following lines:
<PropertyGroup>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<NoWarn>$(NoWarn);1591</NoWarn>
</PropertyGroup>
- - Open the
Startup.cs
file and inConfigureServices
method add following codes toservices.AddSwaggerGen
:
services.AddSwaggerGen(options =>
{
// add a custom operation filter which sets default values
options.OperationFilter<SwaggerDefaultValues>();
var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
options.IncludeXmlComments(xmlPath);
});
- Open
WeatherForecastController
class add following XML comment to theGET
method:
/// <summary>
/// This API returns list weather forecast.
/// </summary>
[HttpGet]
public IEnumerable<WeatherForecast> Get()
Run the application and can see the preceding comment in front API URL:
Additionally, We can use <remarks>
element. The <remarks>
element content can consist of text, JSON, or XML:
/// <summary>
/// This API returns list weather forecast.
/// </summary>
/// <remarks>
/// Possible values could be:
///
/// "Freezing", "Bracing", "Chilly", "Cool", "Mild",
/// "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
///
/// Just for demonstration
///
/// GET api/v1/WeatherForecast
/// {
/// }
/// curl -X GET "https://server-url/api/v1/WeatherForecast" -H "accept: text/plain"
///
/// </remarks>
[HttpGet]
public IEnumerable<WeatherForecast> Get()
The response types and error codes are denoted in the XML comments and data annotations:
/// <response code="200">Returns list of weather forecast</response>
/// <response code="400">Noway, just for demonstration</response>
[HttpGet]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public IEnumerable<WeatherForecast> Get()
For more information about [ProducesResponseType]
, see API conventions.
Step 6 - Hide a property from Swagger
Sometimes you want to hide some properties of the model and you don't want to be visible in Swagger. The only thing that you need to do is decorating the property with [System.Text.Json.Serialization.JsonIgnore]
attribute.
public class DummyModel
{
public string FirstName { get; set; }
public string LastName { get; set; }
[JsonIgnore]
public string FullName { get; set; }
}
Step 7 - Enable JWT Bearer Authorization
To enable Authrozie
button in swagger add the following codes:
services.AddSwaggerGen(options =>
{
...
options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
{
Description = "JWT Authorization header using the Bearer scheme. Example: \"Authorization: Bearer {token}\"",
Name = "Authorization",
In = ParameterLocation.Header,
Type = SecuritySchemeType.ApiKey
});
options.AddSecurityRequirement(new OpenApiSecurityRequirement
{
{
new OpenApiSecurityScheme
{
Reference = new OpenApiReference
{
Type = ReferenceType.SecurityScheme,
Id = "Bearer"
},
Scheme = "oauth2",
Name = "Bearer",
In = ParameterLocation.Header,
},
new List<string>()
}
});
});
Step 8 - Lowercase API URL
To generate lowercase API URLs add this to ConfigureServices
method:
services.Configure<RouteOptions>(options => { options.LowercaseUrls = true; });
You can find the source code for this walkthrough on Github.
Posted on March 1, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
March 16, 2021
March 6, 2021
March 1, 2021