白猫のメモ帳

C#とかJavaとかJavaScriptとかHTMLとか機械学習とか。

ASP.NETアプリケーションをDocker用にマイグレーションする(.NET 7)

コンソールアプリケーションのマイグレーションをしたので、今度はWebのマイグレーションをします。
あるあるな気がしますが、記事にしてないだけでだいぶ前に実施したのでもう記憶が…。

とりあえずプロジェクトを作る

VisualStudioからASP.NET Core Web アプリ(Model-View-Controller)を選んで(MVCなのは元がMVCだからです)、

フレームワークに.NET 7.0を選びます。コンソールアプリと一緒ですね。

こんな感じになります。

プロジェクトファイルがこんな感じで、

<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <TargetFramework>net7.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
  </PropertyGroup>

</Project>

Program.csがこんな感じ。

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddControllersWithViews();

var app = builder.Build();

// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Home/Error");
}
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");

app.Run();

実行するとこんな感じ。

汎用ホストだ

あくまでマイグレーションなので、Webアプリケーションどうやって作るかみたいな話はしません。
ポイントはProgram.csの書き方くらいですね。

汎用ホストの記事を読むとわかるかと思うのですが、このProgram.csの内容が何となく汎用ホストの書き方に似ています。
追ってみるとWebApplication.CreateBuilderが作るWebApplicationBuilderがIHostBuilderの実装クラスであるConfigureHostBuilderを持っているのがわかります。
つまりこの仕組みも汎用ホストで、それをさらにWeb用にラップしているようです。

そういえば汎用ホストを試していたときにバックグラウンドで動いて、自分で停止しない限り動き続けるみたいなことを書きましたね。
よく考えてみるとこの仕様はまさにWeb用にぴったりです。
つまり汎用ホストによってコンソールアプリケーションとWebアプリケーション、バックグラウンドサービスみたいなのが全部同じ仕組みで動くわけですね。

で、汎用ホストと似たようなものだと思えばそんなに難しさもありません。
BuildされるものがIHostじゃなくてWebApplicationになっただけで、それをRunするのは一緒ですね。

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddControllersWithViews();

var app = builder.Build();

// いろいろする

app.Run();

nginx用にごにょごにょ

Dockerではnginxの裏側にアプリケーションを置く想定なのがちょっとややこしいところです。
今回は「/sample」のパスでこのアプリケーションを動かすことにしましょう。

appsettings.jsonにPathBaseという設定を足します。(名前はご自由に)

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "PathBase": "/sample"
}

コンソールアプリのときと同じようにASPNETCORE_ENVIRONMENTにDevelopmentを設定しておいて、appsettings.Development.jsonには書かなければデバッグ時には効かなくなります。
これでデバック時にはリバプロの存在を気にしなくてOKになります。

で、これをProgram.csで既定のパスに設定します。

var pathBase = app.Configuration.GetSection("PathBase").Get<string>();
if (!string.IsNullOrEmpty(pathBase))
{
    app.UsePathBase(pathBase);
}

nginx側の設定はこうなります。(default.conf)
同じネットワークに設定すればコンテナ名で参照できます。

location /sample/ {
    proxy_pass http://SampleContainer/sample/;
}

静的ファイルはnginx側で配信するのでとりあえずUseStaticFilesはいらないです。
認証するのであれば必要ですが、今回はUseAuthorizationもいらないです。

で、最終的にこうなります。

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllersWithViews();
using var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Home/Error");
}
var pathBase = app.Configuration.GetSection("PathBase").Get<string>();
if (!string.IsNullOrEmpty(pathBase))
{
    app.UsePathBase(pathBase);
}

app.UseRouting();

app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");

app.Run();

設定をControllerに渡そう

Controllerの定義を見るとこんな感じになっています。

public class HomeController : Controller
{
    private readonly ILogger<HomeController> _logger;

    public HomeController(ILogger<HomeController> logger)
    {
        _logger = logger;
    }

    public IActionResult Index()
    {
        return View();
    }

    // 略
}

これも汎用ホストのときに見たDIと一緒なので、設定を渡したい場合はこうなります。(thisはただの私の好みです)

public class HomeController : Controller
{
    private readonly IConfiguration configuration;
    private readonly ILogger<HomeController> logger;

    public HomeController(IConfiguration configuration, ILogger<HomeController> logger)
    {
        this.configuration = configuration;
        this.logger = logger;
    }

    public IActionResult Index()
    {
        return View();
    }

    // 略
}

似てる

他はあんまり特別なことをする必要はないかと思います。
汎用ホストを一回見てるととてもわかりやすいですね。

ヌメロニム

雑記。

KubernetesK8sって書いたり、Internationalisationをi18nって書いたりするのをヌメロニム(numeronym)っていうらしい。
そういう略し方するのは知ってたけど名前を知らなかった。

n11n(Normalisation)とかl10n(localization)くらいまではわかる気がするけど、
a11y(Accessibility)とかP13n(Personalisation)とか言われてもわかる気がしない。

Wikipediaを見ると発音ベースの数略語も広義に含まれるらしいので、O2O(Online to Offline)とかもそうなのかな。
略すのではなく同じ文字の繰り返しのW3CWorld Wide Web Consortium)とかS3(Simple Storage Service)とかはどうなんだろう。
日本語でこういう略し方ってあんまりしないよね。

.NETの汎用ホストでコンソールアプリケーションを作ってみる

.NET Framework(4.6.2)のコンソールアプリケーションを.NET(7)にマイグレーションしてみる - 白猫のメモ帳

前回.NET Frameworkから.NETへのマイグレーションをした際にごにょごにょした、設定の読み込みだったりログだったりを便利に使える汎用ホストという機能があるらしいので使ってみます。
いつからあるのかよくわからなかったのですが、.NET Core 3.0あたりからなんでしょうか…。

なんだかいろいろなことができてしまってよくわからないやつなのですが、MSのドキュメントによれば、

''ホスト'' とは、次のようなアプリのリソースと有効期間機能をカプセル化するオブジェクトです。
依存関係の挿入 (DI)
ログの記録
構成
アプリのシャットダウン
IHostedService の実装

だそうです。
.NET 汎用ホスト - .NET | Microsoft Learn

どうやって使うの?

ははーんなるほどこの汎用ホストってやつを使えば設定ファイル読み込みだったりDIだったりもできちゃうわけか、そいつは便利だと思ってドキュメントをざっと読むのですが、使い方がわからなくてフリーズします。
なんだかユーティリティ的なものだと思って読んでいたのが良くなかったのかもしれません。
(汎用ホストって名前汎用的すぎて何を指しているのかがよくわからない)

IHost host = Host.CreateDefaultBuilder(args)
    .ConfigureServices((hostContext, services) =>
    {
        services.AddHostedService<Worker>();
    })
    .Build();

host.Run();

このコードに対して、Hostが作れたのはいいけど私のコードはどこに書くんだって思った時点でそもそもの発想が間違っていたようです。
よく見るとWorkerというクラスがジェネリック指定されてAddHostedServiceというメソッドが呼ばれています。
つまり、このサービスクラスをホストしてねってことのようです。

で、ホストってなんだって言うとここに戻ってきます。

''ホスト'' とは、次のようなアプリのリソースと有効期間機能をカプセル化するオブジェクトです。

なるほどね?
ホストって言葉ってよく聞くんですが、正しく意味を答えろって言われると難しい気がします。コンピュータとかサーバと同義みたいな。
じゃあホストするって?
うーん…サービスを提供できる場所に置くみたいなイメージですかね。ホストはサービスを提供する「何か」かな。
なんかこのあたりの定義結構難しいですね。

まぁ汎用ホストは動かしたいサービスを載せておいて実行するのをサポートしてくれる「何か」なんですかね。
汎用だからこそ汎用ホスト自体の説明をもう少ししてほしい…。

使ってみる

AddHostedServiceを使えば良さそうなことがわかったので使ってみます。
IHostedServiceというインタフェースの実装を作れば良さそう。

求められるのはStartAsyncとStopAsyncの2メソッド。
開始と終了…RunとかExecute的なものじゃないようです。
とりあえずStartAsync側に処理を書いてみます。

class TestService : IHostedService
{
    public Task StartAsync(CancellationToken cancellationToken)
    {
        Debug.WriteLine("なんか処理するよ");
        return Task.CompletedTask;

    }

    public Task StopAsync(CancellationToken cancellationToken)
    {
        return Task.CompletedTask;
    }
}

で、実行してみると、

using var host = Host.CreateDefaultBuilder(args)
                     .ConfigureServices((hostContext, services) => services.AddHostedService<TestService>())
                     .Build();
host.Run();

実行が終わりません。
画像の通りCtrl+Cを押すと止まります。

もう一回説明を見に行きます。
アプリの起動と正常なシャットダウンの制御とあるので、自分でシャットダウンしないとバックグラウンドサービス的に動くんですね。

ホストが起動すると、サービス コンテナーのホステッド サービスのコレクションに登録されている IHostedService の各実装で IHostedService.StartAsync が呼び出されます。 Worker サービス アプリでは、BackgroundService インスタンスを含むすべての IHostedService 実装で、BackgroundService.ExecuteAsync メソッドが呼び出されます。

アプリの相互依存するすべてのリソースを 1 つのオブジェクトに含める主な理由は、アプリの起動と正常なシャットダウンの制御の有効期間の管理のためです。 これを実行するには、Microsoft.Extensions.Hosting NuGet パッケージを使用します。

IHostApplicationLifetimeでホストの終了をコントロール

.NET 汎用ホスト - .NET | Microsoft Learn

なんだかいろいろ書いてありますが、最後に

アプリケーションでホスティングが使用されており、ホストを正常に停止したい場合は、Environment.Exit ではなく IHostApplicationLifetime.StopApplication を呼び出すことができます。

との記載があるのでこの通りにします。
つまりこう。

class TestService : IHostedService
{
    private readonly IHostApplicationLifetime appLifetime;

    public TestService(IHostApplicationLifetime appLifetime)
    {
        this.appLifetime = appLifetime;
    }

    public Task StartAsync(CancellationToken cancellationToken)
    {
        Debug.WriteLine("なんか処理するよ");
        this.appLifetime.StopApplication();
        return Task.CompletedTask;
    }

    public Task StopAsync(CancellationToken cancellationToken)
    {
        return Task.CompletedTask;
    }
}

IHostApplicationLifetimeはコンストラクタに引数で設定しておけば勝手にDIしてくれます。
Startの中でStopするのちょっと気持ち悪いけどしょうがないよね。

LoggerとかConfigurationはどこから

ログの記録、構成も便利にしてくれるらしいのですが、どこから来るのでしょうか。
これはIHostApplicationLifetimeと一緒でコンストラクタに引数設定しておくと勝手にDIしてくれます。

class TestService : IHostedService
{
    private readonly IHostApplicationLifetime appLifetime;
    private readonly ILogger logger;
    private readonly IConfiguration confing;

    public TestService(IHostApplicationLifetime appLifetime, ILogger<TestService> logger, IConfiguration confing)
    {
        this.appLifetime = appLifetime;
        this.logger = logger;
        this.confing = confing;
    }

    public Task StartAsync(CancellationToken cancellationToken)
    {
        this.logger.LogInformation("start");
        this.logger.LogInformation(this.confing.GetValue<string>("SomethingConfiguration"));
        this.appLifetime.StopApplication();
        return Task.CompletedTask;
    }

    public Task StopAsync(CancellationToken cancellationToken)
    {
        this.logger.LogInformation("stop");
        return Task.CompletedTask;
    }
}

ついでに普通のDIも

AddHostedServiceでサービスを登録する際に併せてAddScopedでインタフェースとその実装クラスを指定しておくと、
こちらもコンストラクタインジェクションをしてくれます。

using var host = Host.CreateDefaultBuilder(args)
                     .ConfigureServices((hostContext, services) => services.AddHostedService<TestService>()
                                                                           .AddScoped<ISample, Sample>())
                     .Build();
host.Run();

実装クラスはコンストラクタなしかpublicで引数なしのコンストラクタを実装しておきましょう。

public interface ISample
{
    public string Hoge();
}
public class Sample : ISample
{
    public string Hoge() => "Fuga";
}

で、こうですね。

class TestService : IHostedService
{
    private readonly IHostApplicationLifetime appLifetime;
    private readonly ISample sample;

    public TestService(IHostApplicationLifetime appLifetime, ISample sample)
    {
        this.appLifetime = appLifetime;
        this.sample = sample;
    }

    public Task StartAsync(CancellationToken cancellationToken)
    {
        Debug.WriteLine(this.sample.Hoge());
        this.appLifetime.StopApplication();
        return Task.CompletedTask;

    }

    public Task StopAsync(CancellationToken cancellationToken)
    {
        return Task.CompletedTask;
    }
}

さっきからコンストラクタ引数が増減していますが、引数にあればDIしてくれる融通が利く感じで良きですね。

基底クラスとか作ってみる?

こういうの作ったら便利だったりするんですかね?

public abstract class SimpleConsoleApplicationBase<T> : IHostedService where T : SimpleConsoleApplicationBase<T>
{
    private readonly IHostApplicationLifetime appLifetime;
    protected readonly ILogger logger;
    protected readonly IConfiguration confing;

    public SimpleConsoleApplicationBase(IHostApplicationLifetime appLifetime, ILogger<T> logger, IConfiguration confing)
    {
        this.appLifetime = appLifetime;
        this.logger = logger;
        this.confing = confing;
    }

    async Task IHostedService.StartAsync(CancellationToken cancellationToken)
    {
        await this.ExecuteAsync();
        this.appLifetime.StopApplication();
    }

    Task IHostedService.StopAsync(CancellationToken cancellationToken)
    {
        return Task.CompletedTask;
    }

    protected abstract Task ExecuteAsync();
}

で、こう。

public class TestApplication : SimpleConsoleApplicationBase<TestApplication>
{
    public TestApplication(IHostApplicationLifetime appLifetime, ILogger<TestApplication> logger, IConfiguration confing) : base(appLifetime, logger, confing)
    {
    }

    protected override Task ExecuteAsync()
    {
        base.logger.LogInformation("さんぷるだよ");
        return Task.CompletedTask;
    }
}

うーん。これでいいのかがよくわからない。

結局ユーティリティ的に使えないのかというと

普通にこういうクラスを実行したいとして、

class Simple
{
    protected private ILogger<Simple> logger;
    protected private IConfiguration confing;

    public Simple(ILogger<Simple> logger, IConfiguration confing)
    {
        this.logger = logger;
        this.confing = confing;
    }

    public void Execute()
    {
        this.logger.LogInformation("さんぷるだよ");
    }
}

GetRequiredServiceを使ったりすればRunしないで使えたりもする。

using var host = Host.CreateDefaultBuilder(args).Build();
var logger = host.Services.GetRequiredService<ILogger<Simple>>();
var config = host.Services.GetRequiredService<IConfiguration>();
new Simple(logger, config).Execute();

うーん。これはさすがに違うような気がするな。

というわけで

正解なのかわからないですが、とりあえず便利そうではあるなと思いました。
はい。