What every ASP.NET Core Web API project needs - Part 2 - API versioning and Swagger

moesmp

Mohsen Esmailpour

Posted on March 1, 2021

What every ASP.NET Core Web API project needs - Part 2 - API versioning and Swagger

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;
});
Enter fullscreen mode Exit fullscreen mode

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:
Swagger 1
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:
Project structure 1
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
{
...
Enter fullscreen mode Exit fullscreen mode

Again run the application and you see that api-version input no longer exists:
Swagger 2
Now duplicate WeatherForecastController in V2 folder:
Alt Text

[ApiController]
[ApiVersion("2.0")]
[Route("api/v{version:apiVersion}/[controller]")]
public class WeatherForecast2Controller : ControllerBase
{
...
Enter fullscreen mode Exit fullscreen mode

Now we have 2 versions of WeatherForecast API, nevertheless, we are not able to find Swagger documentation for V2:
Alt Text

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:
Alt Text
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 folder Swagger to the Infrastructure folder.
  • Add a new file SwaggerDefaultValues.cs to Swagger 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;
        }
    }
}
Enter fullscreen mode Exit fullscreen mode
  • Add another file ConfigureSwaggerOptions.cs to Swagger 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;
    }
}
Enter fullscreen mode Exit fullscreen mode
  • Open the Startup.cs file and in ConfigureServices method delete the default swagger configuration:
services.AddSwaggerGen(c =>
{
    c.SwaggerDoc("v1", new OpenApiInfo { Title = "CoolWebApi", Version = "v1" });
});
Enter fullscreen mode Exit fullscreen mode
  • Add the following configuration:
services.AddTransient<IConfigureOptions<SwaggerGenOptions>, ConfigureSwaggerOptions>();
services.AddSwaggerGen(options =>
{
    // add a custom operation filter which sets default values
    options.OperationFilter<SwaggerDefaultValues>();
});
Enter fullscreen mode Exit fullscreen mode
  • In Configure method add IApiVersionDescriptionProvider parameter:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IApiVersionDescriptionProvider provider)
Enter fullscreen mode Exit fullscreen mode
  • In Configure method delete the Swagger UI default configuration:
app.UseSwagger();
app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "CoolWebApi v1"));
Enter fullscreen mode Exit fullscreen mode
  • 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());
});
Enter fullscreen mode Exit fullscreen mode

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).
Alt Text
Now run the application and you can see both APIs documents:
Alt Text

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>
Enter fullscreen mode Exit fullscreen mode
  • - Open the Startup.cs file and in ConfigureServices method add following codes to services.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);
});
Enter fullscreen mode Exit fullscreen mode
  • Open WeatherForecastController class add following XML comment to the GET method:
/// <summary>
/// This API returns list weather forecast.
/// </summary>
[HttpGet]
public IEnumerable<WeatherForecast> Get()
Enter fullscreen mode Exit fullscreen mode

Run the application and can see the preceding comment in front API URL:
Alt Text
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()
Enter fullscreen mode Exit fullscreen mode

Alt Text
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()
Enter fullscreen mode Exit fullscreen mode

Alt Text
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; }
}
Enter fullscreen mode Exit fullscreen mode

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>()
        }
    });
});
Enter fullscreen mode Exit fullscreen mode

Step 8 - Lowercase API URL

To generate lowercase API URLs add this to ConfigureServices method:

services.Configure<RouteOptions>(options => { options.LowercaseUrls = true; });
Enter fullscreen mode Exit fullscreen mode

You can find the source code for this walkthrough on Github.

💖 💪 🙅 🚩
moesmp
Mohsen Esmailpour

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