SourceChord

C#とXAML好きなプログラマの備忘録。最近はWPF系の話題が中心です。

.NET Coreことはじめ~その5・ミドルウェア~

今回はミドルウェアASP.NET Coreのミドルウェアの概念について学んでいきます。

ミドルウェア

ASP.NET Coreでは、Webサーバーがクライアントからリクエストは、ミドルウェアというものを通して処理されます。

ミドルウェアは複数定義され、それらがパイプラインとなって順に実行されていきます。

↓のリンク先の図のようなイメージ。
https://docs.asp.net/en/latest/fundamentals/middleware.html#creating-a-middleware-pipeline-with-iapplicationbuilder

たぶんこの辺の概念は、Node.jsでExpressとかを使ってた人は直感的に理解できるのでは、、と思います。

これらのミドルウェアは、StartupクラスのConfigureメソッドの引数として渡される、IApplicationBuilder型の引数を通して設定をすることができます。
このIApplicationBuilderの引数に対して、Run/Use/Mapなどのメソッドを呼び出して設定を行います。

Runメソッド

今までのサンプルでも何度か使用してきましたが、このメソッドの引数に書いたデリゲートでリクエストに対するレスポンスを定義できます。

以下の例では、レスポンスに「Hello, World!」という文字列を書いて応答します。

        public void Configure(IApplicationBuilder app)
        {
            app.Run(async context =>
            {
                await context.Response.WriteAsync("Hello, World!");
            });
        }

dotnet runで実行し、ブラウザからlocalhostにアクセスすると、Runメソッドで定義したレスポンスが返ってきていることが確認できます。
f:id:minami_SC:20161102122553p:plain

ミドルウェアの終端

Runメソッドは、ミドルウェアのチェーンを終端させます。

そのため、以下のように複数Runメソッドを書いても、最初のRunメソッドで書いた部分しか実行されません。

        public void Configure(IApplicationBuilder app)
        {
            app.Run(async context =>
            {
                await context.Response.WriteAsync("Hello, World!");
            });

            app.Run(async context =>
            {
                // 以下の部分は実行されない。
                await context.Response.WriteAsync("Hello, Again");
            });
        }

Useメソッド

Useメソッドは、ミドルウェアのチェーンに処理を追加します。
Runメソッドとは異なり、処理を終端させないので、Useメソッド呼出し後に別のミドルウェアを繋げることができます。

以下のように、Use/Runメソッドを順に呼び出しておくと、メソッドの呼び出し順に実行されていることが確認できます。

            app.Use(async (context, next) =>
            {
                await context.Response.WriteAsync("Hello, World!");
                // 後続のミドルウェアを呼び出す
                await next.Invoke();
            });

            app.Run(async context =>
            {
                await context.Response.WriteAsync("Hello, Again");
            });

f:id:minami_SC:20161102122855p:plain

next.Invokeメソッド

この関数を呼び出すと、後続のミドルウェアが実行されます。
先ほどの例では、UseでWriteAsyncした後、Runで定義した内容が続けて実行されます。

後続のミドルウェアの処理が終わると、awaitの後の部分が実行されます。 以下のサンプルのように、コンソール出力をしてみると、どのような順で実行されているか確認できます。

        public void Configure(IApplicationBuilder app)
        {
            app.Use(async (context, next) =>
            {
                Console.WriteLine("--[1]--");
                await context.Response.WriteAsync("Hello, World!");
                // 後続のミドルウェアを呼び出す
                await next.Invoke();
                Console.WriteLine("--[3]--");
            });

            app.Run(async context =>
            {
                Console.WriteLine("--[2]--");
                await context.Response.WriteAsync("Hello, Again");
            });
        }

f:id:minami_SC:20161102122603p:plain

ちなみに、このnext.Invoke()というのを呼び出さないと、後続のミドルウェア実行されず、Runメソッドでミドルウェアのチェーンを終端させたような動作になるので注意が必要です。

Mapメソッド

Mapメソッドを使うと、リクエストで受けたURLのパスに応じて、ミドルウェアのチェーンを分岐できます。

以下の例では、/hello/worldでそれぞれ別の処理を実行します。

        public void Configure(IApplicationBuilder app)
        {
            app.Map("/hello", hello => {
                hello.Run(async context => {
                    await context.Response.WriteAsync("This is hello page.");
                });
            });

            app.Map("/world", world => {
                world.Run(async context => {
                    await context.Response.WriteAsync("This is world page.");
                });
            });
        }

それぞれのURLにアクセスすると、以下のように異なったレスポンスが返ります。
f:id:minami_SC:20161102122644p:plain
f:id:minami_SC:20161102122652p:plain

標準で用意されているミドルウェア

ASP.NET Coreでは以下のようなミドルウェアが標準で用意されています。

  • Authentication
  • CORS
  • Routing
  • Session
  • Static Files

https://docs.asp.net/en/latest/fundamentals/middleware.html#built-in-middleware

ここでは、簡単に使える例として、StaticFilesミドルウェアを使い、指定フォルダ内の内容を静的に配信するサーバーを作ってみます。

Static Filesミドルウェアの使用

まず、このミドルウェアを使用するには、project.json のdependenciesの項目に"Microsoft.AspNetCore.StaticFiles": "1.0.0"というのを追記し、dotnet restoreコマンドを実行します。

{
  "version": "1.0.0-*",
  "buildOptions": {
    "debugType": "portable",
    "emitEntryPoint": true
  },
  "dependencies": {
    "Microsoft.Extensions.Logging": "1.0.0",
    "Microsoft.Extensions.Logging.Console": "1.0.0",
    "Microsoft.AspNetCore.StaticFiles": "1.0.0"
  },

Startup.cs

    public class Startup
    {
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            app.UseStaticFiles();
        }
    }

Program.cs
Main関数では、UseContentRoot()という関数を呼び出しておきます。
このメソッドで、Webアプリのルートとなるディレクトリを設定します。

        public static void Main(string[] args)
        {
            var host = new WebHostBuilder().UseKestrel()
                                           .UseContentRoot(Directory.GetCurrentDirectory())
                                           .UseStartup<Startup>()
                                           .UseEnvironment("Production")
                                           .Build();

            host.Run();
        }

また、wwwrootフォルダを作り、その中にWebサーバーで配信したいファイル/フォルダなどを配置します。

ここでは以下のような二つのファイルを作成しました。
f:id:minami_SC:20161102122714p:plain

index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Document</title>
</head>
<body>
    <h1>This is index.html page</h1>
</body>
</html>

hello/world.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Document</title>
</head>
<body>
    <h1>This is hello/world.html page</h1>
</body>
</html>

dotnet runコマンドで実行すると、それぞれwwwrootに配置した通りのファイルが静的に配信できていることが確認できます。

f:id:minami_SC:20161102122723p:plain
f:id:minami_SC:20161102122730p:plain

このような静的なファイルサーバーの実装方法については、以下のドキュメントに詳細情報がまとまっています。
https://docs.asp.net/en/latest/fundamentals/static-files.html

ミドルウェアの作成

ミドルウェアは、独立したクラスとして作成することもできます。
独立したクラスとして作成しておくと、複雑な処理にも対応しやすく、再利用しやすいコードになります。

公式ドキュメントのサンプルと同じように、ここでは受け付けたリクエストのログ出力を行うミドルウェアを作ってみます。

ミドルウェアの定義

まずは、ミドルウェア定義用のクラスを作ります。

    public class RequestLoggerMiddleware
    {
        private readonly RequestDelegate _next;
        private readonly ILogger _logger;

        public RequestLoggerMiddleware(RequestDelegate next, ILoggerFactory loggerFactory)
        {
            this._next = next;
            this._logger = loggerFactory.CreateLogger<RequestLoggerMiddleware>();
        }

        public async Task Invoke(HttpContext context)
        {
            this._logger.LogInformation($"Handling request: {context.Request.Path}");
            await this._next.Invoke(context);
            this._logger.LogInformation("Finished handling request.");
        }
    }

使い方

このミドルウェアを使うには、型引数として先ほど作ったクラスを指定してUseMiddlewareメソッドを呼び出します。

また、ログ出力を伴うミドルウェアなので、Configureメソッドの最初でログ出力を行うためloggerfactory.AddConsole();を実行しています。

        public void Configure(IApplicationBuilder app, ILoggerFactory loggerfactory)
        {
            loggerfactory.AddConsole();
            app.UseMiddleware<RequestLoggerMiddleware>();
動作確認

次のサンプルのように、Mapメソッドでのミドルウェア定義と合わせて書いておくと、それぞれのURLにアクセスした際に、コンソールにリクエストのパス情報が出力されるようになります。

        public void Configure(IApplicationBuilder app, ILoggerFactory loggerfactory)
        {
            loggerfactory.AddConsole();
            app.UseMiddleware<RequestLoggerMiddleware>();

            app.Map("/hello", hello => {
                hello.Run(async context => {
                    await context.Response.WriteAsync("This is hello page.");
                });
            });

            app.Map("/world", world => {
                world.Run(async context => {
                    await context.Response.WriteAsync("This is world page.");
                });
            });
        }

f:id:minami_SC:20161102122744p:plain

拡張メソッドを定義してから使う

以下のような拡張メソッドを定義しておくと、

    public static class RequestLoggerExtensions
    {
        public static IApplicationBuilder UseRequestLogger(this IApplicationBuilder builder)
        {
            return builder.UseMiddleware<RequestLoggerMiddleware>();
        }
    }

このように、拡張メソッドの呼び出し一発で使えるようになります。
また、app.と打った時点でインテリセンスの候補にも出るの便利です。

        public void Configure(IApplicationBuilder app, ILoggerFactory loggerfactory)
        {
            loggerfactory.AddConsole();
            app.UseRequestLogger();
ミドルウェア作成のまとめ

ミドルウェアを作る場合は、以下のようなステップで作成します。

  • ミドルウェア定義用のクラスを作成
  • 上記クラスで、コンストラクタやInvokeメソッドなどを実装し、ミドルウェアの処理を実装
  • IApplicationBuilderに対する拡張メソッドを作って、利用しやすくする

自分でミドルウェアを作る機会がどの程度あるかは、まだよくわかりません。
ですが、ミドルウェアがどのように作られているかを知っておくと、フレームワーク標準のミドルウェアを使うときなどに、内部でどのような処理が行われているか、具体的なイメージを持って実装ができるのでは、と思います。

今回はここまで。