NEXTSCAPE blog

株式会社ネクストスケープの社員による会社公式ブログです。ネスケラボでは、社員が日頃どのようなことに興味をもっているのか、仕事を通してどのような面白いことに取り組んでいるのかなど、会社や技術に関する情報をマイペースに紹介しています。

MENU

ASP.NET Core 6のブートストラップ処理

こんにちは、ネクストスケープでエンジニアをしている醍醐です。
本記事は「NEXTSCAPE Advent Calender 2021」19日目です。

qiita.com

1. はじめに

先日(と言っても、11月のことなのでもう1ヶ月以上経ちました)、.NET 6 が無事GAを迎えました。
去年リリースされた.NET 5とは異なりLTS(Long Term Support)版となります。
ネクストスケープ内では、一部のプロジェクトにおいて昨年より.NET 5を開発に採用するなどの動きもありましたが、LTSが付いたことで世の中の多くのプロジェクトが採用しやすくなったのではないかと思います。
ということで この記事では Visual Studio 2022 で ASP.NET Core 6 プロジェクトを作ってみて、最初にびっくりしたブートストラッププロセスコードの変化について書いていこうと思います。

2. とりあえずWebアプリを作ってみる

Visual Studio 2022を起動し「ASP.NET Core Web アプリ(Model-View-Controller)」プロジェクトを作成します。
プロジェクト名は WebApp6 としました。

f:id:ryuichi_ns:20211218164625p:plain

f:id:ryuichi_ns:20211218164642p:plain

実行すると以下の通り。

f:id:ryuichi_ns:20211218164700p:plain

いつも通りのMVCテンプレが吐いてくれるWebアプリになっています。

Startup.csが無くなったんだけど

プロジェクト構成を確認すると、ASP.NET Core(厳密には.NET Framework時代のOWINが入ってきたあたりから)では馴染み深い Startup.cs が無くなっていました。

f:id:ryuichi_ns:20211218164715p:plain

そして Program.cs を覗くと、これも馴染みのないコードが記述されています。

  • 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");
  // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
  app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

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

app.Run();

このブートストラップコードにおけるポイントを以下に説明したいと思います。

最上位レベルステートメント(top level statement)

Programクラス / Main()メソッドが無く、唐突に実装コードが記述されています。
これはC# 9から導入された「最上位レベルステートメント(top level statement)」機能を使っているためです。
最上位レベルステートメントは、アプリケーション内に1つだけ定義することができます。機能としては単純に「class Program」「Main()」の定義を省略出来ることになります。
また、コンパイル後のWebApp6.dllをILSpyで逆コンパイルしてみると以下のようになっています。

f:id:ryuichi_ns:20211218164736p:plain

つまりコンパイルプロセスで、最上位レベルステートメントに対して「class Program」「Main()」の自動補完が行われたILが出力されています。

Implicit Usings と Global using

usingディレクティブも全て省略されています(ちなみに1行目で使われているWebApplicationクラスはMicrosoft.AspNetCore.Builder名前空間のクラスです)。
これは Implicit Usings機能 と Global using機能 で実現しています。
WebApp6.csprojを見ると以下の記述が確認できます。

<ImplicitUsings>enable</ImplicitUsings>

ImplicitUsings設定がenableになっているとコンパイル時に暗黙的にプロジェクトに必要な名前空間へのusingが追加されます(追加される名前空間はプロジェクトの種類によって異なる)。
usingがどのように追加されるか?ですが、これは Global using 機能が使われています。コンパイルプロセス内で隠蔽されていますが、 コンパイルすると生成されている「WebApp6\obj\Debug\net6.0\WebApp6.GlobalUsings.g.cs」ファイルを見ると分かります。

  • WebApp6.GlobalUsings.g.cs
// <auto-generated/>
global using global::Microsoft.AspNetCore.Builder;
global using global::Microsoft.AspNetCore.Hosting;
global using global::Microsoft.AspNetCore.Http;
global using global::Microsoft.AspNetCore.Routing;
global using global::Microsoft.Extensions.Configuration;
global using global::Microsoft.Extensions.DependencyInjection;
global using global::Microsoft.Extensions.Hosting;
global using global::Microsoft.Extensions.Logging;
global using global::System;
global using global::System.Collections.Generic;
global using global::System.IO;
global using global::System.Linq;
global using global::System.Net.Http;
global using global::System.Net.Http.Json;
global using global::System.Threading;
global using global::System.Threading.Tasks;

globalキーワードが付与されている為、ここでの記述がプロジェクト全体に対して影響します。

Program.cs + Startup.cs の構造から Program.cs 1本で簡潔に

.NET 5以前のASP.NET CoreではStartupクラスを用意し、Program#Main()においてブートストラッププロセスで利用するようにStartupクラスを指定する実装が作法となっていました。
Startupクラスは ConfigureServices()メソッド と Configure()メソッド を実装する作法となっており、2つのメソッドにはそれぞれ以下のような実装を行うことになっていました。

  • ConfigureServices()メソッド:
    アプリケーションで必要とされるサービスのコンテナ(DI)への登録処理等
  • Configure()メソッド:
    ミドルウェアパイプラインの構築処理等

.NET 6では「最上位レベルステートメント / Global using / Implicit Usings」と併せて、記述がシンプルになるようなブートストラップ構造の改善がなされたと見ればよいと思います。
とはいえ、構成すべき情報が大きくなるプロジェクトの場合、従来通りのStartup形式であったり、もしくは構成定義部分を別のクラスに切り出して、それらをProgram.csから呼び出すような形式にすることも考えられると思います。

※.NET 6の新規プロジェクト作成時のテンプレートはProgram.cs1本形式ですが、.NET5までの従来通りのProgram + Startup形式も引き続きサポートされており、.NET 5以前の実装を.NET 6への移行する際も無理に新形式に変更する必要はないことがMSのdocsでも書かれています。

WebApplicationBuilderクラス

もう少しProgram.csの実装の中身を見てみます。
1行目の WebApplication.CreateBuilder(args) で作成されているのは「WebApplicationBuilder」オブジェクトです。
WebApplicationBuilderのI/Fをざっくり表すと以下になります。

public sealed class WebApplicationBuilder {
  public IWebHostEnvironment Environment { ,,, }
  public IServiceCollection Services { ... }
  public ConfigurationManager Configuration { ,,, }
  public ILoggingBuilder Logging { ... }
  public ConfigureWebHostBuilder WebHost { ... }
  public ConfigureHostBuilder Host { ... }
  
  public WebApplication Build() { ... }
}

過去のASP.NET Coreを知っている方であれば、何となくイメージがわきますね。
「IWebHostEnvironment Environment / IServiceCollection Services / ConfigurationManager Configuration」あたりは、以前のStartupクラス実装でメソッド引数として受け取っていたオブジェクトと同じのものです(あるいは同等のもの)。
つまり、WebApplicationBuilderオブジェクト経由でブートストラップ処理に必要な各オブジェクトにアクセスできる仕組みになりました。

過去に遡り、ASP.NET Core 1.1のブートストラップ処理では「UseKestrel()やAddJsonFile("appsettings.json")やStartupクラス定義や・・・」必要な定義が多数ありましたが、バージョンアップと共にデフォルト定義が内部に隠蔽され、ユーザコードが最小構成になるように改善されてきた結果が今回のASP.NET Core 6でのプロジェクトテンプレートの形になっています。

3. autofacを入れてみる

なにかコードをいじってみたいなぁ・・・ということで、利用するDIコンテナ(IoCコンテナ)をautofacにしてみたいと思います。

1) nugetを追加

Nugetで「autofac」「autofac.Extensions.DependencyInjection」を追加します。

f:id:ryuichi_ns:20211218164759p:plain

2) インジェクション対象オブジェクトをプロジェクトに追加

インジェクション対象のインターフェイスと実装クラスをを用意します。
以下のような "次の数値を取得できるカウンターオブジェクト" とします。

  • utils\ICounter.cs
namespace WebApp6.Utils
{
  public interface ICounter
  {
    int Next();
  }
}
  • utils\MyPoorCounter.cs
namespace WebApp6.Utils
{
  public class MyPoorCounter : ICounter
  {
    private int _count = 0;

    public int Next()
    {
      return ++_count; // インメモリでカウントアップするだけ
    }
  }
}

3) ブートストラップ処理でautofacを構成

Program.csに、サービスプロバイダのファクトリーとしてAutofacを利用するように定義します。
また、ICounterインターフェイスに対してはMyPoorCounterオブジェクトをシングルトンで解決するようにコンテナ構成を行います。

  • Program.cs
using Autofac;
using Autofac.Extensions.DependencyInjection;
using WebApp6.Utils;

var builder = WebApplication.CreateBuilder(args);

builder.Host.UseServiceProviderFactory(
  new AutofacServiceProviderFactory() );
builder.Host.ConfigureContainer<ContainerBuilder>(builder =>
{
  builder.RegisterType<MyPoorCounter>().As<ICounter>().SingleInstance();
}
);

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

var app = builder.Build();
...省略

4) コントローラで利用する

コントローラーに対してコンストラクタインジェクションでカウンターオブジェクトを割り当てて、利用します。

  • HomeController.cs
public class HomeController : Controller
{
  private readonly ICounter _counter;
  private readonly ILogger<HomeController> _logger;

  public HomeController(
    ILogger<HomeController> logger,
    ICounter counter)
  {
    _logger = logger;
    // コンストラクタインジェクションでシングルトンなICounter(実体はMyPoorCounterオブジェクト)を取得
    _counter = counter;
  }

  public IActionResult Index()
  {
    ViewBag.CurrentCounter = _counter.Next(); // カウンターを利用
    return View();
  }
  ...省略
}

4. [おまけ] Startupはマジッククラスだった

.NET5以前の話に戻りますが、Startupクラス自体がマジッククラスだったという点は、もともと実装の理解が難しい点でもあったかと思います。
.NET 5でのProgram.csのデフォルトの実装は以下でした。

public class Program
{
  public static void Main(string[] args)
  {
    CreateHostBuilder(args).Build().Run();
  }

  public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
      .ConfigureWebHostDefaults(webBuilder =>
      {
        webBuilder.UseStartup<Startup>();
      });
}

Startup.csの実装は以下のようになります。

public class Startup
{
  public Startup(IConfiguration configuration)
  {
    Configuration = configuration;
  }

  public IConfiguration Configuration { get; }

  public void ConfigureServices(IServiceCollection services) { ...省略 }

  public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { ...省略 }
}

こうするとエントリーポイントであるProgram#Main()がランタイム環境より呼び出され、次にStartupクラスのコンストラクタが呼び出され、更にConfigureServices()メソッド、Configure()メソッドと順番に呼び出されるブートストラッププロセスが実行されます。
例えば、Startupクラスがフレームワークで定義された何らかのインターフェイスをインプリメントする仕組みであれば理解しやすいのですが、「UseStartup<T>で指定したユーザ定義型クラスの特定メソッドが呼び出される仕組み」というのが分かりにくさを増長させていた部分でもあると思います。
実際にはCreateHostBuilder(args)がMicrosoft.Extensions.Hosting.HostBuilderインスタンスを返却し、そのBuild()メソッド呼び出しから以下のようなメソッド呼び出し階層を経て動的にConfigureService() / Configure()が呼び出されます。

Microsoft.Extensions.Hosting.HostBuilder#Build()
 -> Microsoft.AspNetCore.Hosting.GenericWebHostBuilder#UseStartup()
   -> Microsoft.AspNetCore.Hosting.StartupLoader#FindConfigureServicesDelegate()

※↓参考↓:ConfigureServices() / Configure()メソッドを動的に見つけている実装のコード片 github.com

5. まとめ

ASP.NET Coreのブートストラッププロセスはバージョンアップと共になかなか激しく変化(改善)してきています。
ただし、このプロセスについては、そこまで深堀しなくてもASP.NET CoreによるWebアプリ開発は出来るでしょう。
1.1の頃からの比較で言えば、入口の敷居をどんどん下げる方向、もしくは書かなければいけないコードを減らす方向で改善が続けれらているのだと思います。
.NET 6は待望のLTS版ですので、今後 積極的に実運用開発に取り入れていきたいですね。

※本記事を執筆するにあたり、改めて過去のASP.NET Coreのブートストラップコードを確認しようと思い、.NET各バージョンでプロジェクト作成を行ってみました。せっかくなのでASP.NET Coreの歴史の変遷であるコード片を以下に置いておきます。

github.com