LoginSignup
12
9

More than 5 years have passed since last update.

ASP.NET Core Middleware Fundamentals - 日本語意訳

Last updated at Posted at 2017-03-22

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 のリクエストパイプラインは、次の図に示すように(実行スレッドは黒矢印に従って)、次々に呼び出される一連のリクエストデリゲートで構成されています。:

Request processing pattern showing a request arriving, processing through three middlewares, and the response leaving the application. Each middleware runs its logic and hands off the request to the next middleware at the next() statement. After the third middleware processes the request, it's handed back through the prior two middlewares for additional processing after the next() statements each in turn before leaving the application as a response to the client.

各デリゲートは、次のデリゲートの前後に操作を実行できます。デリゲートはまた、次のデリゲートにリクエストを渡さないように決定することもでき、これは、リクエストパイプラインの短絡と呼ばれます。短絡はしばしば望ましいものです。なぜなら不必要な作業を回避することができるからです。例えば、Static file Middleware は、静的ファイルのリクエストを返し、残りのパイプラインを短絡することができます。Exception-handling デリゲートは、パイプラインの早い段階で呼ばれる必要があります。そうすれば、パイプラインの後の段階で発生する例外をキャッチすることができます。

可能な限り簡単な ASP.NET Core アプリケーションは、すべてのリクエストを処理するただ一つのリクエストデリゲートをセットアップします。このケースでは、実際のリクエストパイプラインは含まれていません。その代わり、すべての HTTP リクエストに対するレスポンスの中で、単一の無名関数が呼び出されます。

C#
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 パラメーターを呼ばないことによってパイプラインを短絡できることを覚えておきましょう。) 次のサンプルで説明するように、一般的に、次のデリゲートの前後でアクションを実行することができます。:

C#
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 コンポーネントを追加します。

  1. Exception/error handling
  2. Static file server
  3. Authentication
  4. MVC
C#
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 レスポンスは圧縮できます。

C#
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 メソッドは、指定したパスとリクエストが一致した場合、リクエストパイプラインを分岐します。リクエストパスが指定されたパスで始まる場合、分岐が行われます。

C#
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)が使われています。

C#
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 は、クエリ文字列から現在のリクエストのカルチャをセットします。:

C#
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 を、インラインの代わりにクラスで作成したものになります。:

C#
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 を公開します。:

C#
using Microsoft.AspNetCore.Builder;

namespace Culture
{
    public static class RequestCultureMiddlewareExtensions
    {
        public static IApplicationBuilder UseRequestCulture(
            this IApplicationBuilder builder)
        {
            return builder.UseMiddleware<RequestCultureMiddleware>();
        }
    }
}

次のコードでは、Configure 内で Middleware を呼び出します。:

C#
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 によって引数で受け取ることができるようになります。例:

c#
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);
    }
}

リソース

12
9
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12
9