- 2017/03/23(Thu) 時点の情報に基づいています
- 公式ドキュメント:ASP.NET Core Middleware Fundamentals
ASP.NET Core Middleware 基礎編
Middleware とは何か
Middleware とは、リクエストとレスポンスを処理するためのアプリケーションパイプラインとして組み立てられたソフトウェアです。各コンポーネントは、パイプラインの次のコンポーネントにリクエストを渡すかどうかを選択し、次のコンポーネントが呼び出される前後に任意のアクションを実行することができます。リクエストデリゲートは、リクエストパイプラインを構築するために使用されます。リクエストデリゲートは、各 HTTP リクエストを処理します。
リクエストデリゲートは、Startup クラスの Configure メソッドに渡される IApplicationBuilder インスタンスの Run, Map, Use 拡張メソッドを使用して構成されます。個々のリクエストデリゲートは、匿名メソッド(インライン Middleware と呼ばれます)としてインラインで指定することも、再利用可能なクラスで定義することもできます。これらの再利用可能なクラスおよびインライン匿名メソッドが Middleware または Middleware コンポーネント です。リクエストパイプライン中の各 Middleware コンポーネントは、パイプライン内の次のコンポーネントを呼び出す役割を担います、あるいは、必要に応じてチェインを短絡します。
Migrating HTTP Modules to Middleware では、ASP.NET Core におけるリクエストパイプラインの以前のバージョンとの違いを説明し、多くの Middleware サンプルを提供します。
IApplicationBuilder を使用した Middleware パイプラインの作成
ASP.NET Core のリクエストパイプラインは、次の図に示すように(実行スレッドは黒矢印に従って)、次々に呼び出される一連のリクエストデリゲートで構成されています。:
各デリゲートは、次のデリゲートの前後に操作を実行できます。デリゲートはまた、次のデリゲートにリクエストを渡さないように決定することもでき、これは、リクエストパイプラインの短絡と呼ばれます。短絡はしばしば望ましいものです。なぜなら不必要な作業を回避することができるからです。例えば、Static file Middleware は、静的ファイルのリクエストを返し、残りのパイプラインを短絡することができます。Exception-handling デリゲートは、パイプラインの早い段階で呼ばれる必要があります。そうすれば、パイプラインの後の段階で発生する例外をキャッチすることができます。
可能な限り簡単な ASP.NET Core アプリケーションは、すべてのリクエストを処理するただ一つのリクエストデリゲートをセットアップします。このケースでは、実際のリクエストパイプラインは含まれていません。その代わり、すべての HTTP リクエストに対するレスポンスの中で、単一の無名関数が呼び出されます。
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
public class Startup
{
public void Configure(IApplicationBuilder app)
{
app.Run(async context =>
{
await context.Response.WriteAsync("Hello, World!");
});
}
}
最初の app.Run デリゲートは、パイプラインを終了します。
app.Use メソッドを使って、複数のリクエストデリゲートをつなげることができます。next パラメーターは、パイプライン内の次のデリゲートを表します。(next パラメーターを呼ばないことによってパイプラインを短絡できることを覚えておきましょう。) 次のサンプルで説明するように、一般的に、次のデリゲートの前後でアクションを実行することができます。:
public class Startup
{
public void Configure(IApplicationBuilder app)
{
app.Use(async (context, next) =>
{
// Do work that doesn't write to the Response.
await next.Invoke();
// Do logging or other work that doesn't write to the Response.
});
app.Run(async context =>
{
await context.Response.WriteAsync("Hello from 2nd delegate.");
});
}
}
WARNING
nextを呼び出した後にHttpResponseを変更する際は注意してください。なぜなら、レスポンスが既にクライアントに送信されてしまっているかもしれないからです。HttpResponse.HasStarted を使ってレスポンスヘッダーが送信されかどうかをチェックできます。
WARNING
writeメソッドを呼んだ後に
next.Invokeを呼ばないでください。Middleware コンポーネントは、レスポンスを生成するか、next.Invokeを呼ぶかのどちらかを行いますが、両方を行うことはできません。
順序付け
Configure メソッドの中で Middleware コンポーネントが追加される順序は、そのままリクエストに対してコンポーネントが呼び出される順序となり、また、レスポンスに対してはその逆順となります。この順序付けは、セキュリティ、パフォーマンス、機能にとって非常に重要です。
Configure メソッドは(下記参照) 、次の Middleware コンポーネントを追加します。
- Exception/error handling
- Static file server
- Authentication
- MVC
public void Configure(IApplicationBuilder app)
{
app.UseExceptionHandler("/Home/Error"); // Call first to catch exceptions
// thrown in the following middleware.
app.UseStaticFiles(); // Return static files and end pipeline.
app.UseIdentity(); // Authenticate before you access
// secure resources.
app.UseMvcWithDefaultRoute(); // Add MVC to the request pipeline.
}
上記のコードでは、UseExceptionHandler がパイプラインに追加された最初の Middleware コンポーネントです。従って、後の呼び出しで発生するどんな例外もキャッチします。
Static file Middleware はパイプラインの早い段階で呼ばれるので、残りのコンポーネントを経由することなくリクエストを処理し短絡を行うことができます。Static file Middleware は認証チェックを行いません。wwwroot 配下にあるファイルを含む Static file Middleware によって提供されるファイルはすべてパブリックに利用可能です。静的ファイルを保護する方法については、Working with static files を参照してください。
リクエストが Static file Middleware で処理されない場合、Identity Middleware(app.UseIdentity) に渡され、認証チェックが行われます。Identity Middleware は認証されていないリクエストを短絡しません。Identity Middleware はリクエストを認証しますが、認可(および拒否)は MVC が特定のコントローラーとアクションを選択した後にのみ発生します。
次の例は、Response compression Middleware よりも先に Static file Middleware によって静的ファイルのリクエストが処理される順序を表しています。この Middleware の順序では、静的ファイルは圧縮されません。UseMvcWithDefaultRoute からの MVC レスポンスは圧縮できます。
public void Configure(IApplicationBuilder app)
{
app.UseStaticFiles(); // Static files not compressed
// by middleware.
app.UseResponseCompression();
app.UseMvcWithDefaultRoute();
}
Run, Map, Use メソッド
Run, Map, Use メソッドを使って HTTP パイプラインを構成します。Run メソッドはパイプラインを短絡します(つまり、次のリクエストデリゲートを呼びません)。Run メソッドは規約であり、Middleware コンポーネントによっては、パイプラインの最後で実行される Run[Middleware] メソッドを公開している場合があります。
Map* メソッドはパイプラインを分岐するための規約として使用されます。Map メソッドは、指定したパスとリクエストが一致した場合、リクエストパイプラインを分岐します。リクエストパスが指定されたパスで始まる場合、分岐が行われます。
public class Startup
{
private static void HandleMapTest1(IApplicationBuilder app)
{
app.Run(async context =>
{
await context.Response.WriteAsync("Map Test 1");
});
}
private static void HandleMapTest2(IApplicationBuilder app)
{
app.Run(async context =>
{
await context.Response.WriteAsync("Map Test 2");
});
}
public void Configure(IApplicationBuilder app)
{
app.Map("/map1", HandleMapTest1);
app.Map("/map2", HandleMapTest2);
app.Run(async context =>
{
await context.Response.WriteAsync("Hello from non-Map delegate. <p>");
});
}
}
次の表は、前のコードを使用した http://localhost:1234 からのリクエストとレスポンスを示しています。
| Request | Response |
|---|---|
| localhost:1234 | Hello from non-Map delegate. |
| localhost:1234/map1 | Map Test 1 |
| localhost:1234/map2 | Map Test 2 |
| localhost:1234/map3 | Hello from non-Map delegate. |
Map が使用された場合、一致したURLセグメントは HttpRequest.Path から取り除かれ、HttpRequest.PathBase に追加されます。
MapWhen は、指定した述語(predicate)の結果が true である場合、リクエストパイプラインを分岐します。Func<HttpContext, bool> 型であればどんな述語(predicate)でも使うことができます。次の例では、クエリ文字列に branch が含まれているかどうかを検出するために述語(predicate)が使われています。
public class Startup
{
private static void HandleBranch(IApplicationBuilder app)
{
app.Run(async context =>
{
var branchVer = context.Request.Query["branch"];
await context.Response.WriteAsync($"Branch used = {branchVer}");
});
}
public void Configure(IApplicationBuilder app)
{
app.MapWhen(context => context.Request.Query.ContainsKey("branch"),
HandleBranch);
app.Run(async context =>
{
await context.Response.WriteAsync("Hello from non-Map delegate. <p>");
});
}
}
次の表は、前のコードを使用した http://localhost:1234 からのリクエストとレスポンスを示しています。
| Request | Response |
|---|---|
| localhost:1234 | Hello from non-Map delegate. |
| localhost:1234/?branch=master | Branch used = master |
Map は入れ子をサポートしています。例:
app.Map("/level1", level1App => {
level1App.Map("/level2a", level2AApp => {
// "/level1/level2a"
//...
});
level1App.Map("/level2b", level2BApp => {
// "/level1/level2b"
//...
});
});
Map は複数のURLセグメントを一度に照合することもできます。例:
app.Map("/level1/level2", HandleMultiSeg);
ビルトイン Middleware
ASP.NET Core には、次の Middleware コンポーネントが同梱されています。:
| Middleware | Description |
|---|---|
| Authentication | 認証をサポートします。 |
| CORS | Cross-Origin Resource Sharing を構成します。 |
| Response Caching | レスポンスのキャッシュサポートを提供します。 |
| Response Compression | レスポンスの圧縮サポートを提供します。 |
| Routing | リクエストのルーティングを定義し制約します。 |
| Session | ユーザーセッションの管理をサポートします。 |
| Static Files | 静的ファイルの提供とディレクトリ参照をサポートします。 |
| URL Rewriting Middleware | URL書き換えとリクエストのリダイレクトをサポートします。 |
Middleware を作る
Middleware は一般に、クラスにカプセル化され、拡張メソッドで公開されます。次の Middleware を考えてみましょう。この Middleware は、クエリ文字列から現在のリクエストのカルチャをセットします。:
public class Startup
{
public void Configure(IApplicationBuilder app)
{
app.Use((context, next) =>
{
var cultureQuery = context.Request.Query["culture"];
if (!string.IsNullOrWhiteSpace(cultureQuery))
{
var culture = new CultureInfo(cultureQuery);
CultureInfo.CurrentCulture = culture;
CultureInfo.CurrentUICulture = culture;
}
// Call the next delegate/middleware in the pipeline
return next();
});
app.Run(async (context) =>
{
await context.Response.WriteAsync(
$"Hello {CultureInfo.CurrentCulture.DisplayName}");
});
}
}
注意:上記のサンプルコードは、Middleware コンポーネントの作成を示すために使用されているだけです。ASP.NET Coreのビルトインローカリゼーションサポートについては、
Globalization and localization を参照してください。
例えば、http://localhost:7997/?culture=no のようにカルチャを渡すことでミドルウェアをテストできます。
次のコードは、前述の Middleware を、インラインの代わりにクラスで作成したものになります。:
using Microsoft.AspNetCore.Http;
using System.Globalization;
using System.Threading.Tasks;
namespace Culture
{
public class RequestCultureMiddleware
{
private readonly RequestDelegate _next;
public RequestCultureMiddleware(RequestDelegate next)
{
_next = next;
}
public Task Invoke(HttpContext context)
{
var cultureQuery = context.Request.Query["culture"];
if (!string.IsNullOrWhiteSpace(cultureQuery))
{
var culture = new CultureInfo(cultureQuery);
CultureInfo.CurrentCulture = culture;
CultureInfo.CurrentUICulture = culture;
}
// Call the next delegate/middleware in the pipeline
return this._next(context);
}
}
}
次の拡張メソッドは IApplicationBuilder を介して Middleware を公開します。:
using Microsoft.AspNetCore.Builder;
namespace Culture
{
public static class RequestCultureMiddlewareExtensions
{
public static IApplicationBuilder UseRequestCulture(
this IApplicationBuilder builder)
{
return builder.UseMiddleware<RequestCultureMiddleware>();
}
}
}
次のコードでは、Configure 内で Middleware を呼び出します。:
public class Startup
{
public void Configure(IApplicationBuilder app)
{
app.UseRequestCulture();
app.Run(async (context) =>
{
await context.Response.WriteAsync(
$"Hello {CultureInfo.CurrentCulture.DisplayName}");
});
}
}
Middleware は、明示的な依存関係の原則に従い、その依存関係をコンストラクタ引数として公開すべきです。 Middleware はアプリケーションライフタイムごとに一度だけ生成されます。もしリクエスト内でミドルウェアとサービスを共有する必要がある場合は、以下のリクエストごとの依存関係を参照してください。
Middleware コンポーネントは、Dependency Injection によってコンストラクタ引数から依存関係を解決することができます。
UseMiddleware<T> can also accept additional parameters directly.
UseMiddleware<T> を使えば、追加引数を直接受け取ることもできます。
リクエストごとの依存関係
Middleware は、リクエストごとではなく、アプリケーションの起動時に生成されるため、 Middleware のコンストラクタで注入された
Scoped Service(リクエストごとに生成される限定された生存期間をもつ Service) は、リクエストごとに他のクラスに注入される Scoped Service とは別物となり共有されません。もし Middleware と他のクラスで Scoped Service を共有しなければならないときは、Invoke メソッドの引数に追加してください。Dependency Injection によって引数で受け取ることができるようになります。例:
public class MyMiddleware
{
private readonly RequestDelegate _next;
public MyMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task Invoke(HttpContext httpContext, IMyScopedService svc)
{
svc.MyProperty = 1000;
await _next(httpContext);
}
}
