Create Open API Schema (Swagger) during your automated build

A few years ago, I was assigned on a project with a friend of mine, Marnix van Valen and we needed to update our APIs in API Management with the latest Open API schema for each release. As we don’t like to do this work manually, it got added to our build- and release pipeline. I like this approach, as it removes the need to host Swagger / Open API compute on my own service and only static files need to be hosted in some folder.

So I started re-implementing the approach we did a couple of years ago.
Turns out, this still works! Well, it would if the packages were still kept up-to-date with the latest ASP.NET features.

No minimal API support

Like many engineering teams, we adopted to use the (recommended) minimal APIs to create the services. Apparently, this doesn’t play nice with the Swashbuckle tooling. There are multiple issues at GitHub on this topic. All without a resolution, aside to migrating back to the ‘old’ way of working with a Program.cs and Startup.cs class.
I understand the core maintainers have different priorities nowadays, but this estimated 15-minute task now took up more time from my part.

Only running on .NET 7

There’s another problem I encountered. The Swagger CLI 6.5.0 tooling only has support up to .NET 7. Our build agents don’t have this version installed anymore.
Lucky for me, there’s an environment variable you can set to make sure the latest version of .NET is being used.

$Env:DOTNET_ROLL_FORWARD = "LatestMajor"

Use the SwaggerHostFactory

The way the tooling is working is by hosting the service and extract the Open API schema from it. This is fine if your service is only doing startup tasks when starting up. As it should!

However, in the real world, sometimes shortcuts are taken for…reasons.
Because of those shortcuts (or ‘features’) my service couldn’t start proper without contacting a dozen of services when starting up.

I don’t think I’m the only person in having this issue, because there are two classes you can use to create your custom startup logic, called SwaggerHostFactory and SwaggerWebHostFactory. The methods in these classes are invoked by convention.
So, if you need to overcome some of the custom logic you’re doing in the default startup, this is the place to look for. I’ve used this to make sure some of the ‘features’ aren’t invoked anymore when the Swashbuckle CLI tooling is used.

What I ended up with

The first thing I needed to do is create/add/modify the Program.cs and Startup.cs files again and make use of the constructs over there like we’ve been doing for years. Not a hard thing to do, but rather annoying.

The second thing I ended up doing is create the SwaggerHostFactory. The internals look a bit like this.

/// <summary>
/// Host factory for Open API tooling
/// </summary>
/// <remarks>
/// For reference:
/// </remarks>
public class SwaggerHostFactory
    /// <summary>
    /// The host creation, used by the Open API tooling to create the contract.
    /// Necessary to omit services from running when the service starts.
    /// </summary>
    /// <returns></returns>
    public static IHost CreateHost()
        return Program.CreateHostBuilder(new string[0]).Build();

And of course, the script to create the specs.

$currentLocation = Get-Location
$outputDirectory = "$currentLocation/openapi-specs/"
$buildEnvironment = "Release"
$aspnetEnvironment = "Development"
$cpuArchitecture = "x64"

$firstServiceBinary = "path/to/my/first.service.dll"
$secondServiceBinary = "path/to/my/second.service.dll"

$apiVersion = "v1"

dotnet tool update --tool-path . --version 6.* swashbuckle.aspnetcore.cli

Function CreateOpenAPISpecFileFor($binaryFilePath, $openAPISpecFileName) {
    New-Item -Path "$($outputDirectory)" -ItemType Directory -Force 
    $binary = Split-Path $binaryFilePath -leaf
    $path = Split-Path $binaryFilePath
    $toolsPath = Get-Location
    Set-Location $path
    &"$($toolsPath.Path)\swagger" tofile --output "$outputDirectory$openAPISpecFileName" $binary $apiVersion

# swagger.exe is built using .net 7 and not compatible with .net 8 (yet) but it should work, therefore rolling forward
$Env:DOTNET_ROLL_FORWARD = "LatestMajor"

CreateOpenAPISpecFileFor $firstServiceBinary "first-service.json"
CreateOpenAPISpecFileFor $secondServiceBinary "second-service.json"

I’m very sure this can be optimized quite a bit, but it’ll give you the gist of it.
Depending on what type build agent you’re using, you probably want to update the $outputDirectory to reflect the location where you need the static files to be stored at. You can even put it in some folder of your web application if you want it to host those files by itself. But I’d advice in using API Management, like we did in Marnix’ earlier linked post.


comments powered by Disqus