xin9le.net

Microsoft の製品/技術が大好きな Microsoft MVP な管理人の技術ブログです。

Swashbuckle.AspNetCore.Cli で FileLoadException が出る場合の対処

最近社内で「CI で swagger.json を生成したいから対応してほしい」と言われてやってみました。そして思いっきりハマりました。ということで忘れないようにメモ。

ちなみに本内容は執筆時点での最新版である「.NET 7 + Swashbuckle.AspNetCore (v6.5.0)」で再現しているもので、バージョンが異なると挙動が変わる可能性があります。その点はご了承ください。

TL;DR

  1. SwaggerHostFactory.CreateHost を利用する
  2. 古き良き 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()
    { }  // 省略
}

実際にやってみる

では先の書き方に従って実装していきましょう。僕は次のように実装してみました。ポイントとして SwaggerHostFactoryinternal でも問題ありません。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 しか情報がなく解決に大変苦労しました。同じような問題にぶち当たっている方の一助になれば幸いです。