Generating Stellar OpenAPI Specs

This guide by our development team shows you how to generate stellar OpenAPI specs.

23.07.2024

Development

Nowadays, OpenAPI Specifications are the de facto standard for describing and documenting APIs, enabling easy interoperability between API providers and API clients and consumers.

Producing a high quality OpenAPI spec will have a significant impact on the success of your API as it is the basis for a lot of tools that sit higher in the stack like:

  • swagger UI
  • developer portals
  • autogenerated API clients (language agnostic)

In the .NET world, the most common way to generate an API spec is by using swagger. When properly configured, swagger supports versioning and automatic documentation based on the autogenerated XML docs.

Hence the better and more complete the XML docs, the better the resulting documentation and specification of the API.

In the next sections, we will be putting it all together from a 0 to a hero API spec that is:

  • versioned
  • serves multiple audiences
  • well documented
  • automation ready

Enable Versioning

The first step is fairly simple. We enable versioning and configure swagger:

public static IServiceCollection AddVersioning(this IServiceCollection services)
{
    services.AddApiVersioning(options =>
       {
            options.DefaultApiVersion = new ApiVersion(0, 0);
            options.AssumeDefaultVersionWhenUnspecified = true;
            options.ReportApiVersions = true;
        })
        .AddApiExplorer(options =>
         {
             options.GroupNameFormat = "'v'VVV";
             options.SubstituteApiVersionInUrl = true;
         });

    return services;
}
Copy code

The configuration part is as generic as possible to be reusable in multiple different APIs:

public static IServiceCollection AddSwagger(this IServiceCollection services,
string apiTitle, string apiDescription)
{
services.AddSingleton(new SwaggerApiMetadata(apiTitle, apiDescription));
services.ConfigureOptions();

return services.AddSwaggerGen(c: SwaggerGenOptions =>
{
//* ... */

var xmlDocumentationFilePaths = Directory.GetFiles(AppContext.BaseDirectory,
"*.xml", SearchOption.TopDirectoryOnly).ToList();

foreach (var fileName in xmlDocumentationFilePaths)
{
var xmlFilePath = Path.Combine(AppContext.BaseDirectory, fileName);
if (File.Exists(xmlFilePath))
{
c.IncludeXmlComments(xmlFilePath, includeControllerXmlComments: true);
}
}
});
}
Copy code

The above are used like this from an API project:

builder.Services.AddVersioning();
builder.Services.AddSwagger("Connect Readings API",
														"Provides endpoints to get entity readings.");
Copy code

Now let’s unpack what is going on.

  • In lines 4 and 5, the SwaggerApiMetadata record is needed to be able to encapsulate and then inject the API properties specified at compile time (the title and description) in the SwaggerGenOptions configurator at runtime.
  • Just before the code in line 7 will execute, the ConfigureSwaggerOptions configurator will run and will set the metadata for each version defined in the APIs. The configurator looks something like this:
class ConfigureSwaggerOptions(IApiVersionDescriptionProvider provider,
 SwaggerApiMetadata apiMetadata) : IConfigureOptions
{
    public void Configure(SwaggerGenOptions options)
    {
        foreach (var versionDescription in provider.ApiVersionDescriptions)
        {
            options.SwaggerDoc(versionDescription.GroupName,
											            new OpenApiInfo
													        {
												            Title = apiMetadata.Title,
												            Version = versionDescription.ApiVersion.ToString(),
												            Description = apiMetadata.Description
													        });
        }
    }
    //shortened for brevity
}
Copy code

Line 11 to 21 will pick up on all XML doc files that are found and loaded into swagger.

Line 9 is a placeholder for features detailed in the next section.

Customising Swagger Generation

There are times when you want to make programmatic changes to the way the final OpenAPI spec json file is generated. For example, exposing in the OpenAPI spec an HTTP header that is used by all endpoints, which in turn will show up on the UI to be easily filled out by the user.

For this, we can use swagger filters. Think of filters like .NET middleware but for swagger.

services.AddSwaggerGen(c: SwaggerGenOptions =>
{
   c.DocumentFilter();
   c.DocumentFilter();
   c.OperationFilter();
   // ...
}
Copy code

Document filter applies to the whole document while the operation filters are applied for each endpoint path.

Everything so far can be nicely packaged in a shared nuget library to be reused across multiple API services.

Multiple API Audiences

It could often be the case where you might have in the same API service both public endpoints that are exposed to partners via a gateway and privateinternal ones that are only used by your team to carry out admin tasks or what not.

The internal endpoints will be part of the same logical API version, ex: v1, v2, v3 etc. but should be hidden from the public spec that the partners really see, and on the other hand still be visible in the internal swagger UI where your team (developers) can have access.

Considering the infrastructure pieces from previous sections are in place there are only two things left to do:

  • define internal versions of the API
[ApiController]
[ApiVersion("1.0")]
[ApiVersion("1.0-internal")]
[Produces("application/json")]
[Route("v{version:apiVersion}/entities")]
public sealed class MyController : ApiController
{
...
}
Copy code
  • map endpoints to the either public, private or both type of versions
[HttpGet("last")]
[MapToApiVersion("1.0")]
[MapToApiVersion("1.0-internal")]
[ProducesResponseType(typeof(MyResponse), 200)]
public async Task> GetResponseAsync(...)
{
...
}
Copy code

Strictly speaking there are two asp.net versions defined but only one logical version (1.0). If you leave out one version, that endpoint will simply not appear in the generated spec for that asp.net version.

This will generate a swagger UI that will allow us to pick which definition to use:

Fine Tuning

Usually you will need to fine tune the specs based on the audience. For example, if we want to expose the spec in a developer portal that will probably have a different host server than the localhost version in swagger UI. The ideal place to make these conditional tweaks is in swagger filters.

For instance, the AddServerFilter from the previous section can look something like this:

public class AddServerFilter : IDocumentFilter
{
    public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)
    {
        var url = "https://platform.domain.com/api"; //

        if (!context.DocumentName.Contains("-internal"))
        {
            swaggerDoc.Servers.Add(new OpenApiServer { Url = url });
        }
    }
}
Copy code

This will add the following attribute in the resulting OpenAPI spec, that will be later picked up by developer portal tooling and UI.

"servers": [
  {
    "url": "https://platform.domain.com/api"
  }
],
Copy code

Mapping XML Docs to Swagger UI

The quality of the resulting OpenAPI spec is determined from the quality on the XML docs, attributes and signature and data types of the API endpoints. The more complete they are, the easier it is to use swagger UI or any other similar tool based on OpenAPI specifications.

Take the following code as a source:

/// 
/// Returns the last reading
/// 
/// The id of the entity.
/// The type of the reading data.
/// A  that represents the asynchronous operation.
/// Returns the most recent normalized reading for an entity.
[HttpGet("last")]
[MapToApiVersion("1.0")]
[MapToApiVersion("1.0-internal")]
[ProducesResponseType(typeof(Connect.Api.Models.Reading.V1.Reading), 200)]
public async Task> GetLastReadingAsync([BindRequired] [FromRoute] Guid entityId,
        [FromQuery] string? type = "electricity")
Copy code

This will generate the following swagger UI. Notice the entityId example and default value for type. They are both pre populated and ready to use. Notice the remarks section which is totally optional and not added by default. Once present, it will give the API endpoint a more detailed description.

Other tools provide similar capabilities and will offer the clients of the API a greatly improved experience.

Using the same XML docs in the models that are used as part of the endpoint interface, we obtain a nicely documented schema. Notice all the descriptions in the response schema below:

Next Steps

At this point, we have a well documented spec with multiple versions for both internal and public clients that is fine-tuned to work locally but also for publishing to a publicly accessible location from where partners can try out our API.

The next step will be to put in place some automation that will do this publishing automatically and keep it in sync with the code throughout the lifetime of the API.

 

Coming up: Our next technical article will look at real time syncing of API documentation to Readme.io.