Program.cs
に記載されるDIやミドルウェアといったASP.NET Core 基本的な概念を解説します。
とりあえずチュートリアルなどでアプリを動かしてみたが、その裏側でどんなことが行われているのかよくわからないという方が対象です。
仕組みを知らなくてもアプリの開発はできますが、本格的な開発を行う上では必ず理解しておきたい内容です。
環境
- Windows 11
- Visual Studio 2022
- .NET 8
Program.csとは
ASP.NET Core アプリの起動時に最初に読み込まれるファイルで、アプリの実行に必要な設定が行われます。
※ .NET 5 以前はStartup.cs
とProgram.cs
の2ファイルで構成されていましたが、.NET 6 以降はProgram.cs
に統一されシンプルになりました。
Program.cs
はASP.NET Coreのテンプレート(Razor pages, MVC, Blazor, Web API)すべてに存在しますが、中の記載はテンプレートによって異なります。
以下はRazor pagesプロジェクトのデフォルトのProgram.cs
です。(一部コメントを追加)
// WebApplicationBuilderクラスのインスタンスを生成
var builder = WebApplication.CreateBuilder(args);
// サービスをコンテナに登録する
builder.Services.AddRazorPages();
// WebApplicationのインスタンスを生成
var app = builder.Build();
// パイプラインにミドルウェアを登録する
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
app.MapRazorPages();
// アプリケーションの実行
app.Run();
Program.cs
でやっていることをざっくり大別すると、コンテナへのサービスの登録(DI)とパイプラインへのミドルウェアの登録に大きく分けられます。
以降ではその2つを中心に詳しく見ていきます。
依存性の注入(DI)
まず、非常に重要な概念であるDI (Dependency Injection) について説明します。
※もしDIを学ぶのが初めての場合は、先にSOLID原則の「依存関係逆転の原則」を理解しておくことをおすすめします。
ASP.NET CoreにおけるDIの基本
DIをざっくりと説明すると、オブジェクトAが別のオブジェクトBに依存している場合に、そのオブジェクトBを外部から受け取れるようにする手法で、依存関係を解消し、結果的にテスト容易性などを上げることができます。
ASP.NET Core ではデフォルトでDIの仕組みが用意されており、事前に注入したいオブジェクト(ASP.NET Core ではサービスと呼ばれる)をDIコンテナに登録しておくと、アプリ全体からそのサービスを利用することができるようになります。
Program.cs
の以下の箇所でDIコンテナへのサービスの登録を行っています。
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorPages();
1行目でWebApplicationBuilder
クラスをインスタンス化していますが、その際にASP.NET Core で提供される多くのサービス(ILogger
など)が自動でDIコンテナに登録されます。
3行目ではRazorPagesに必要なサービスを追加で登録しています。(プロジェクトの種類により記載が異なります)
自作サービスを登録したい場合は次のように記述します。
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddScoped<ISampleRepository, SampleRepository>();
builder.Services.AddRazorPages();
AddScoped
拡張メソッドの後に、登録したいサービスのインターフェースとその実装クラスを記述します。
サービスを登録するための拡張メソッドは複数用意されています。
今回のようにAddScoped
で登録されたサービスは、Webアプリの場合、1リクエストごとにインスタンスが生成され、レスポンスを返したらインスタンスが破棄されます。
他の拡張メソッドについてはドキュメント「サービスの有効期間」をご参照ください。
登録した自作サービスを呼び出すには、コンストラクタの引数に利用するインターフェースを指定します。
※コンストラクタで依存するオブジェクトを注入するので、「コンストラクタインジェクション」といいます
public class SampleService
{
private readonly ISampleRepository _sampleRepository;
public SampleService(ISampleRepository sampleRepository)
{
_sampleRepository = sampleRepository;
}
public void Get()
{
var data = _sampleRepository.Get();
Console.WriteLine(data);
}
}
このようにすることで、SampleServiceは具象クラスではなくインターフェースに依存するようになるので、ISampleRepositoryを実装するモッククラスなどへの置き換えが可能になり、結果的にテストがやりやすくなります。
参考ドキュメント
パイプラインとミドルウェア
次に、同じく重要な概念であるパイプラインとミドルウェアの基本を説明します。
基本的な仕組み
パイプラインとは、HTTPリクエストを受け取ってからHTTPレスポンスを返すまでの一連の流れを指します。
パイプラインは通常、複数のミドルウェアで構成され、順番に呼び出されます。
ミドルウェアとは、HTTPリクエストとHTTPレスポンスを処理するために、パイプラインに組み込まれたソフトウェア(クラスやメソッド)のことです。
ルーティングを行うもの、エラー処理を行うもの、認証を行うものなど、様々な役割を持つミドルウェアが存在します。
パイプラインとミドルウェアの関係は公式ドキュメントの図が参考になります。
リクエストが来たら最初のミドルウェアが呼ばれ、next()
が実行されると次のミドルウェアが呼び出されます。
next()
を実行しない場合は次のミドルウェアを呼び出さずにパイプラインが終了します。
ミドルウェアの動きを確認するため、Razor PagesのProgram.cs
を以下のように書き換えてみましょう。
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
// Useメソッドでミドルウェア(デリゲート)をパイプラインに追加する
app.Use(async (context, next) =>
{
// レスポンスにメッセージを書き込む
await context.Response.WriteAsync("First Middleware ");
// 次のミドルウェアを呼び出す
await next.Invoke();
});
// Runメソッドを実行するとパイプラインが終了する(パイプラインの最後に実行する)
app.Run(async context =>
{
await context.Response.WriteAsync("Second Middleware");
});
app.Run();
パイプラインにミドルウェアを登録するにはUseメソッドやRunメソッドを使用します。
実行後にlocalhostにアクセスすると、レスポンスにメッセージが書き込まれたことが確認できます。
組み込みのミドルウェア
ASP.NET Coreでは標準で様々なミドルウェアが用意されており、Program.cs
では、以下でミドルウェアの登録を行っています。
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
app.MapRazorPages();
UseXXXはミドルウェアごとに定義されている拡張メソッドで、呼び出すとパイプラインに登録することができます。
ただしミドルウェアは呼び出す順番がある程度決められているため、ミドルウェアの登録を追加する場合は下記ドキュメントを参考にしてください。
カスタムミドルウェア
パイプラインには自作したミドルウェアを登録することもできます。
上で紹介したように、UseメソッドやRunメソッドの中にインラインで処理を記述することも可能ですが、別にクラスを定義して、それを拡張メソッドで呼び出す方法が一般的です。
ここでは詳しくは紹介しませんが、作成方法などは下記ドキュメントを参照してください。
環境変数
そのほかに、開発するうえで重要な環境変数についても解説します。
アプリを起動するとまず、ASPNETCORE_ENVIRONMENT
環境変数の内容が取得され、その値はIWebHostEnvironment
インターフェースを実装する変数に格納されます。
Program.cs
の以下の部分では、ASPNETCORE_ENVIRONMENT
環境変数にDevelopmentが設定されているかどうかを判定しています。
// 開発環境でない場合は例外フィルターとHSTSの設定を行う
if (!app.Environment.IsDevelopment())
{
// 本番環境用の例外処理を担うミドルウェアを有効化
app.UseExceptionHandler("/Error");
// HSTSミドルウェアを有効化 ※HSTSはHTTP通信を強制的にHTTPS通信に切り替える仕組み
app.UseHsts();
}
ASPNETCORE_ENVIRONMENT
はVisual Studio 2022のデバッグプロパティから確認することができ、デフォルトではDevelopmentが設定されています。
試しにhttpsプロファイルを複製し、作成されたプロファイルのASPNETCORE_ENVIRONMENT
の値をStagingに設定してみます。
※ASPNETCORE_ENVIRONMENT
の値を設定しない場合は自動でProduction設定になります。
※ここで追加したプロファイルはlaunchSettings.json
に自動で追加されます。
Program.csのif文にブレークポイントを置いたうえで、作成したプロファイルを選択してアプリを起動すると、if文の中に入る(=開発環境でない)ことが確認できます。