昔は ASP.NET で Swagger / OpenAPI 定義を出力するには Swashbuckle が定番でしたが、あまりアクティブではない時期が長かったせいか最近は NSwag を使うようにしています。
今では GitHub の Star 数も Swashbuckle より NSwag の方が多くなっています。
Docs でも Swashbuckle と共に NSwag を使ったドキュメントが公開されています。
機能的には Swashbuckle と NSwag で大きな差はないですが、NSwag は MSBuild 向けパッケージや CLI を使った静的な Swagger / OpenAPI 定義の生成が行えます。
Swagger / OpenAPI 正義は実行時にわざわざリフレクションを使用して生成する必要はゼロで、ビルド時や CI といったタイミングで生成できれば全く問題ないです。なので 2 つの方法で自動生成を行ってみます。
ローカルビルド時に OpenAPI 定義を生成する
手っ取り早いのが Visual Studio / .NET Core CLI でのビルド時に OpenAPI 定義を同時に生成してしまう方法です。NSwag の場合は NSwag.MSBuild
をインストールすると比較的簡単に実現できます。
NSwag を使った静的な OpenAPI 定義の生成には nswag.json
ファイルを用意した方が楽なので、今回の場合は以下のような定義を用意しておきました。
ASP.NET Core 向けのジェネレータを使って、ビルド済みアセンブリから定義を生成します。ファイルは csproj と同じディレクトリに置いておくと色々と楽です。
{ "runtime": "NetCore31", "documentGenerator": { "aspNetCoreToOpenApi": { "output": "openapi.json", "outputType": "OpenApi3", "assemblyPaths": [ "bin/$(Configuration)/netcoreapp3.1/WebApplication1.dll" ] } } }
正直アセンブリパスの指定が面倒な感じですが、ビルド後に実行する処理なので仕方ない感じです。Visual Studio から生成物のフルパスを渡すことも出来ると思いますが、まずはシンプルな設定にしました。
ビルド後に NSwag を実行するために、csproj に Build 後のターゲットを追加します。Wiki に書いてある通りで大体は問題ないですが、今回は .NET Core 3.1 向けなので NSwagExe
を 3.1 向けにします。
<Project Sdk="Microsoft.NET.Sdk.Web"> <PropertyGroup> <TargetFramework>netcoreapp3.1</TargetFramework> </PropertyGroup> <ItemGroup> <PackageReference Include="NSwag.AspNetCore" Version="13.5.0" /> <PackageReference Include="NSwag.MSBuild" Version="13.5.0"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> </PackageReference> </ItemGroup> <Target Name="NSwag" AfterTargets="Build"> <Exec Command="$(NSwagExe_Core31) run nswag.json /variables:Configuration=$(Configuration)" /> </Target> </Project>
これで Visual Studio や .NET Core CLI を使ったビルドを開始すると、アプリケーションのビルド後に NSwag が実行されて OpenAPI 定義が生成されるようになりました。
出力された OpenAPI 定義をリポジトリに含めて、そのままコミットすることになると思いますが、この方法だとビルドを行わない限りは新しいバージョンが生成されないので、ビルドを忘れると古い OpenAPI 定義のままになってしまいます。
なので Pull Request 向けの CI では、最新の OpenAPI 定義が出力されているかの確認が必要でしょう。
CI では間違いなくアプリケーションのビルドを行うので、そのタイミングで最新の OpenAPI 定義が生成されます。そして HEAD との差分が発生していれば、生成忘れなのでエラーにすれば良いですね。Azure Pipelines を利用して以下のような Pipeline を作成しました。
trigger: - master pool: vmImage: 'ubuntu-latest' steps: - checkout: self persistCredentials: true clean: true - script: dotnet build displayName: Build application - script: | if [ -n "$(git status --porcelain)" ]; then git status exit 1 fi displayName: Ensure up to date
git status --porcelain
を使う方法は openapi-generator の CI を参考にしました。openapi-generator は最新のコードが生成されていないと CI で落ちるようになっているので、今回の目的と完全に一致しました。
例えばビルドせずに API 実装を変更してコミットした場合、以下のように CI で落ちるようになります。
もちろんビルドしなおしてコミットすれば CI は成功します。
Pull Request で実行しておけば、master に古い定義が混ざることを防ぐことが出来ます。OpenAPI 定義をコードから生成している場合は、最低限このくらいのチェックは入れておきたいところです。
CI を使って OpenAPI 定義を生成する
Azure Pipelines などの CI を使って OpenAPI 定義のチェックを行うなら、むしろ CI で生成すれば良いという話にもなるので、Azure Pipelines で最新のコミットから OpenAPI 定義を生成するようにしてみます。
NSwag には CLI が提供されていますが、何故か .NET Core Global Tool としては提供されていないので、NPM で公開されているものを使うのが楽です。
今回も MSBuild を使った時と同様に nswag.json
を作成しますが、アセンブリのパスではなくプロジェクトを指定する方法を選んでみました。アセンブリのパスは不要です。
{ "runtime": "NetCore31", "documentGenerator": { "aspNetCoreToOpenApi": { "project": "WebApplication1.csproj", "output": "openapi.json", "outputType": "OpenApi3" } } }
このファイルを使って npx nswag
を呼び出せば OpenAPI 定義が生成されます。
最後に生成された OpenAPI 定義をリポジトリにコミットする必要があるので、Azure Pipelines で各種 Git コマンドが使えるように、ドキュメントを確認しつつセットアップします。
Build Service に対して git push
のための権限を付ける必要がありますが、ドキュメントの内容が微妙に古くてはまりました。
以下のように Users に中にある Build Service に対して権限を付けると、コマンドが通るようになりました。
今回作成した Pipeline は以下のようになります。どのタイミングで OpenAPI 定義を生成させるべきか悩みましたが、master にマージされた時で良いかなと思ったので、シンプルな形になっています。
Pull Request でも都度生成させることは出来ますが、feature branch に対してサーバー側でコミットを積んでいくと、ブランチが先に進んでしまうので手間かなと思いました。
trigger: - master pool: vmImage: 'ubuntu-latest' steps: - checkout: self persistCredentials: true clean: true - script: dotnet build displayName: Build application - script: npx nswag run WebApplication1/nswag.json /runtime:NetCore31 displayName: Generate OpenAPI document - script: | git config --global user.email "me@shibayan.jp" git config --global user.name "Tatsuro Shibamura" git checkout master git status git add . git commit -m "[skip ci] Update OpenAPI document" git push displayName: Commit new document
変更点がない場合は途中で処理を抜けても良いですが、オプションを付けない限り空のコミットは生成されないので、今のコマンドでも問題はないです。
Git の user.email
と user.name
は自動生成したことが分かるように別名にした方が良いでしょうが、今回は Azure Pipelines での git push
を行うテストも兼ねていたので適当なものを設定しています。
この状態で master に対して何かコミットすると、Azure Pipelines が最新の OpenAPI 定義を生成して、その定義をリポジトリに push してくれます。
ちなみにコミットメッセージには [skip ci]
など CI をスキップするための文字列を入れないと、コミットがトリガーになるので CI がループします。
作成されたコミットを確認すると、CI で自動生成された OpenAPI 定義が含まれています。
これで OpenAPI 定義の生成を意識しなくても、常に CI によって最新の定義が生成されるようになりました。今回はリポジトリに追加しましたが、Azure Storage にコピーして公開するといった方法も選べます。
逆に公開されている OpenAPI 定義から Azure Pipelines などを使って自動的にクライアントを生成して、リポジトリにコミットするという処理も簡単に組めそうです。また別途試してみたいところです。