最近社内で「CI で swagger.json を生成したいから対応してほしい」と言われてやってみました。そして思いっきりハマりました。ということで忘れないようにメモ。
ちなみに本内容は執筆時点での最新版である「.NET 7 + Swashbuckle.AspNetCore (v6.5.0)」で再現しているもので、バージョンが異なると挙動が変わる可能性があります。その点はご了承ください。
TL;DR
SwaggerHostFactory.CreateHost
を利用する- 古き良き Startup クラスを使って
IHost
を生成する
発生した問題を再現してみる
最小プロジェクトを準備
細かいことは省略しつつ、大まかに以下のようなミニマムな ASP.NET Core MVC プロジェクトを生成します。
// Program.cs var builder = WebApplication.CreateBuilder(args); var services = builder.Services; services.AddControllers(); services.AddSwaggerGen(); var app = builder.Build(); app.UseSwagger(); app.UseSwaggerUI(); app.MapControllers(); app.Run();
// SampleApiController.cs [ApiController] [SkipStatusCodePages] [Route("api/sample")] public class SampleApiController : ControllerBase { [AllowAnonymous] [HttpGet("foo")] public IActionResult Foo() => this.Ok(); }
この状態で /swagger
を実行すると以下のように正常に Swagger UI が表示されますし、/swagger/v1/swagger.json
も開くことができます。非常に順調です。あとはこの swagger.json を CLI で出力できるようにするだけです。
{ "openapi": "3.0.1", "info": { "title": "WebApplication2", "version": "1.0" }, "paths": { "/api/sample/foo": { "get": { "tags": [ "SampleApi" ], "responses": { "200": { "description": "Success" } } } } }, "components": {} }
CLI 出力を試みる
では Swashbuckle.AspNetCore.Cli をローカルツールとして導入して swagger.json を出力してみましょう。コマンドは以下のような感じです。
// インストール dotnet new tool-manifest dotnet tool install --local Swashbuckle.AspNetCore.Cli
// 出力 dotnet swagger tofile --output swagger.json WebApplication2/bin/Debug/net7.0/WebApplication2.dll v1
すると以下のように FileLoadException
が出力されます。...なんでやねん!
Unhandled exception. System.IO.FileLoadException: The given assembly name was invalid. File name: 'WebApplication2/bin/Debug/net7.0/WebApplication2.dll' at System.Reflection.AssemblyNameParser.ThrowInvalidAssemblyName() at System.Reflection.AssemblyNameParser.Parse() at System.Reflection.AssemblyNameParser.Parse(String name) at System.Reflection.AssemblyName..ctor(String assemblyName) at Microsoft.AspNetCore.Hosting.StartupLoader.FindStartupType(String startupAssemblyName, String environmentName) at Microsoft.AspNetCore.Hosting.GenericWebHostBuilder.ScanAssemblyAndRegisterStartup(HostBuilderContext context, IServiceCollection services, WebHostBuilderContext webhostContext, WebHostOptions webHostOptions) at Microsoft.AspNetCore.Hosting.GenericWebHostBuilder.<.ctor>b__6_2(HostBuilderContext context, IServiceCollection services) at Microsoft.AspNetCore.Hosting.BootstrapHostBuilder.RunDefaultCallbacks() at Microsoft.AspNetCore.Builder.WebApplicationBuilder..ctor(WebApplicationOptions options, Action`1 configureDefaults) at Microsoft.AspNetCore.Builder.WebApplication.CreateBuilder(String[] args) at Program.<Main>$(String[] args) in C:\Users\xin9le\Documents\Visual Studio 2022\Projects\WebApplication1\WebApplication2\Program.cs:line 1 at System.RuntimeMethodHandle.InvokeMethod(Object target, Void** arguments, Signature sig, Boolean isConstructor) at System.Reflection.MethodInvoker.Invoke(Object obj, IntPtr* args, BindingFlags invokeAttr) --- End of stack trace from previous location --- at Microsoft.Extensions.Hosting.HostFactoryResolver.HostingListener.CreateHost() in C:\projects\ahoy\src\Swashbuckle.AspNetCore.Cli\HostFactoryResolver.cs:line 271 at Microsoft.Extensions.Hosting.HostFactoryResolver.<>c__DisplayClass8_0.<ResolveHostFactory>b__0(String[] args) in C:\projects\ahoy\src\Swashbuckle.AspNetCore.Cli\HostFactoryResolver.cs:line 75 at Swashbuckle.AspNetCore.Cli.HostingApplication.GetServiceProvider(Assembly assembly) in C:\projects\ahoy\src\Swashbuckle.AspNetCore.Cli\HostingApplication.cs:line 72 at Swashbuckle.AspNetCore.Cli.Program.GetServiceProvider(Assembly startupAssembly) in C:\projects\ahoy\src\Swashbuckle.AspNetCore.Cli\Program.cs:line 152 at Swashbuckle.AspNetCore.Cli.Program.<>c.<Main>b__0_4(IDictionary`2 namedArgs) in C:\projects\ahoy\src\Swashbuckle.AspNetCore.Cli\Program.cs:line 82 at Swashbuckle.AspNetCore.Cli.CommandRunner.Run(IEnumerable`1 args) in C:\projects\ahoy\src\Swashbuckle.AspNetCore.Cli\CommandRunner.cs:line 68 at Swashbuckle.AspNetCore.Cli.CommandRunner.Run(IEnumerable`1 args) in C:\projects\ahoy\src\Swashbuckle.AspNetCore.Cli\CommandRunner.cs:line 59 at Swashbuckle.AspNetCore.Cli.Program.Main(String[] args) in C:\projects\ahoy\src\Swashbuckle.AspNetCore.Cli\Program.cs:line 121
対処方法
StackTrace を見るに、どうも Swashbuckle.AspNetCore.Cli は指定したアセンブリの Main メソッドをダイレクトに実行するようです。その過程で Startup クラスを探そうとして失敗している、と。しかし .NET 6 以降で標準テンプレートとなった Top Level Statement で初期化 / 実装をしているので Startup クラスはもうありません。どうも Top Level Statement に対応していない感じですね。
ではどう対処するのかというと、Swashbuckle.AspNetCore.Cli の実行時にのみ特別に解釈される IHost
の生成処理を利用します。アセンブリ内にある特定の型名とメソッド名から IHost
を返してあげればそれを利用するようになっています。特別扱いするのは以下のどちらかの書き方です。所謂ダックタイピング的アプローチですね。
public class SwaggerHostFactory { public static IHost CreateHost() { } // 省略 }
public class SwaggerWebHostFactory { public static IWebHost CreateWebHost() { } // 省略 }
実際にやってみる
では先の書き方に従って実装していきましょう。僕は次のように実装してみました。ポイントとして SwaggerHostFactory
は internal
でも問題ありません。Startup
クラスは同一ファイル内でしか利用しないので file
スコープで大丈夫です。また、ここでも UseStartup
をしない書き方で IHost
を生成することは認められていません。同様に実行時エラーになります。
internal sealed class SwaggerHostFactory { public static IHost CreateHost() => Host .CreateDefaultBuilder() .ConfigureWebHostDefaults(static x => x.UseStartup<Startup>()) .Build(); } file sealed class Startup { public void ConfigureServices(IServiceCollection services) { services.AddControllers(); services.AddSwaggerGen(); } public void Configure(IApplicationBuilder _) { } }
ここまで実装したら改めて dotnet swagger tofile
コマンドを実行してみましょう。無事に出力されるはずです。めでたし ×2。
公式 GitHub にもこのような対処方法については書かれていないですし、実行時例外の StackTrace しか情報がなく解決に大変苦労しました。同じような問題にぶち当たっている方の一助になれば幸いです。