最近社内で「CI で swagger.json を生成したいから対応してほしい」と言われてやってみました。そして思いっきりハマりました。ということで忘れないようにメモ。
ちなみに本内容は執筆時点での最新版である「.NET 7 + Swashbuckle.AspNetCore (v6.5.0)」で再現しているもので、バージョンが異なると挙動が変わる可能性があります。その点はご了承ください。
TL;DR
SwaggerHostFactory.CreateHost
を利用する
- 古き良き Startup クラスを使って
IHost
を生成する
発生した問題を再現してみる
最小プロジェクトを準備
細かいことは省略しつつ、大まかに以下のようなミニマムな ASP.NET Core MVC プロジェクトを生成します。
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();
[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 しか情報がなく解決に大変苦労しました。同じような問題にぶち当たっている方の一助になれば幸いです。