What every ASP.NET Core Web API project needs - Part 2 - API versioning and Swagger
Tags: architecture, dotnet, aspnetcore, webapi
Published: 2021-03-01 15:57:29

In my [previous article](https://dev.to/moesmp/what-every-asp-net-core-web-api-project-needs-part-1-serilog-o5a), 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](https://docs.microsoft.com/en-us/aspnet/core/tutorials/getting-started-with-swashbuckle?view=aspnetcore-5.0&tabs=visual-studio) 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](https://github.com/Microsoft/api-guidelines/blob/master/Guidelines.md#12-versioning), by URL, HTTP header, etc. We are going to add API versioning by URL.

###Step 1 - Install package
Open the [cool-webpi](https://github.com/mo-esmp/cool-webapi) project and Install [Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer](https://www.nuget.org/packages/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:
```csharp
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:
![Swagger 1](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hrpg3snmk9hglbwcxl6w.jpg)
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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4lcb2e9daer0chcmg9nv.jpg)
Open `WeatherForecastController` file and add the `ApiVersion` attribute and modify the `Route` attribute value:
```csharp
[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:
![Swagger 2](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0crjh3aklcuutvky192j.jpg)
Now duplicate `WeatherForecastController` in V2 folder:
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/og3av9uo7f8t4il7yzzv.jpg)
```csharp
[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:
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bl12pi7ctl39fk7m840d.jpg)

Check out the official [API versioning] (https://github.com/microsoft/aspnet-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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ql3i9gvlba6vukdj3t0c.jpg)
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:
```csharp
/// <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` to `Swagger` folder and copy following codes:
```csharp
/// <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 in `ConfigureServices` method delete the default swagger configuration:
```csharp
services.AddSwaggerGen(c =>
{
    c.SwaggerDoc("v1", new OpenApiInfo { Title = "CoolWebApi", Version = "v1" });
});
```
- Add the following configuration:
```csharp
services.AddTransient<IConfigureOptions<SwaggerGenOptions>, ConfigureSwaggerOptions>();
services.AddSwaggerGen(options =>
{
    // add a custom operation filter which sets default values
    options.OperationFilter<SwaggerDefaultValues>();
});
```
- In `Configure` method add `IApiVersionDescriptionProvider` parameter:
```csharp
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IApiVersionDescriptionProvider provider)
```

 - In `Configure` method delete the Swagger UI default configuration:
```csharp
app.UseSwagger();
app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "CoolWebApi v1"));
```
 - Add the following codes to configure Swagger UI:
```csharp
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). 
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tzesfodx6kc9z8b00ebq.jpg)
Now run the application and you can see both APIs documents:
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hzmw229ofmwrgxdbye76.jpg)

###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:
```xml
<PropertyGroup>
  <GenerateDocumentationFile>true</GenerateDocumentationFile>
  <NoWarn>$(NoWarn);1591</NoWarn>
</PropertyGroup>
```
- - Open the `Startup.cs` file and in `ConfigureServices` method add following codes to `services.AddSwaggerGen`:
```csharp
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 the `GET` method:
```csharp
/// <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:
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/dtlq1422zipg5nhi97t9.jpg)
Additionally, We can use `<remarks>` element. The `<remarks>` element content can consist of text, JSON, or XML:
```csharp
/// <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()
```
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/owk239swx2uait5jy06t.jpg)
The response types and error codes are denoted in the XML comments and data annotations:
```csharp
/// <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()
```
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5wjbqw0puwpn5pi0sws8.jpg)
For more information about `[ProducesResponseType]`, see [API conventions](https://docs.microsoft.com/en-us/aspnet/core/web-api/advanced/conventions?view=aspnetcore-5.0).

###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.
```csharp
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:
```csharp
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:
```csharp
services.Configure<RouteOptions>(options => { options.LowercaseUrls = true; });
```

You can find the source code for this walkthrough on [Github](https://github.com/mo-esmp/cool-webapi).