パイプラインの流れ
パイプラインは一つ以上のミドルウェアから構成され、各ミドルウェアの役割はHttpContextを基にHTTPリクエストを処理し、Responseに必要な情報を設定し、最終的なレスポンス出力を完了させることです。パイプラインの流れメカニズムを理解することで、パイプラインを利用したインターセプターやカスタムミドルウェアの実装など、高度な操作が可能になります。パイプラインのデータフローは以下の通りです:リクエストがミドルウェア1に入り、HttpContext(正確にはそのResponseオブジェクト)にロジックを重ね、次にnext()を呼び出してミドルウェア2に移行し、このプロセスが順次繰り返されます。すべてのロジックが重ね終わった後、最終的にフロントエンドにレスポンスが返されます。
パイプラインの実装
IApplicationBuilderを使用してパイプラインを構築する方法は主に2つあります:UseメソッドとRunメソッドです。これらの違いについては後述します。
app.Useメソッド
まず、Useメソッドの最初のオーバーロードを見てみましょう。これはデリゲート型のミドルウェアです。
このミドルウェアはnextというRequestDelegateデリゲートも受け取り、次のパイプラインミドルウェアを呼び出すことができます。以下のコード例を見てみましょう:
app.Use(async (context, next) =>
{
await context.Response.WriteAsync("最初の処理……");
await next.Invoke();
});
上記の使い方は初心者には少し分かりにくいかもしれませんが、問題なく動作します。しかし、内部ではどのように実装されているかご存知でしょうか?ここでは言語の高度な機能であるデリゲートが使用されており、デリゲートを通じてオープン・クローズ原則が実現されています。つまり、パイプラインの拡張性が開放され、app.Useメソッドを規定通り使用できますが、内部で定義されたデリゲートパラメータの型(例:contextやFunc<task>など)は実装を隠蔽しています。そのため、next.Invoke()は具体的な実装をカプセル化したものであり、使用者は内部実装を気にせずに利用できます。
app.Useにはもう一つのオーバーロードがあります。ここではRequestDelegateを引数として受け取り、RequestDelegateを返すFuncが渡されます。最初のオーバーロードとは異なり、next.Invoke()の呼び出しはなく、直接RequestDelegateをappに返します。
app.Run
app.Runとapp.Useの主な違いは、app.Useが次のパイプラインミドルウェアを呼び出せるのに対し、app.Runは呼び出せない点です。以下のコードでその違いを示します:
app.Use(async (context, next) =>
{
await context.Response.WriteAsync("<html><body>");
await context.Response.WriteAsync("<div>app.Useで定義されたミドルウェア内</div>");
await next();
await context.Response.WriteAsync("</body></html>");
});
app.Run(async context => {
await context.Response.WriteAsync("<div>app.Runで定義されたミドルウェア内</div>");
});
// このミドルウェアは出力されますか?
app.Use(async (context, next) =>
{
await context.Response.WriteAsync("<html><body>");
await context.Response.WriteAsync("<div>もう一つのapp.Useで定義されたミドルウェア</div>");
await next();
await context.Response.WriteAsync("</body></html>");
});
上記のコードでは、Runの後に定義された2番目のUseは出力されません。これはRunが後続のパイプラインミドルウェアを呼び出さないためです。そのため、一般的にはRunメソッドをすべてのパイプラインミドルウェアの最後に配置するのが慣例です。