多分わかりやすいASP.NET Core Webアプリケーション

こんにちは、サイオステクノロジー技術部 武井です。今回は、ASP.NET CoreによるWebアプリケーション作成の基本のキをご説明したいと思います。

ASP.NET Coreとは?

マルチプラットフォーム(Windows、Linux、Mac)で動作する.NET環境です。最初は、Windowsのみで動作する.NETに内包されていましたが、.NET5.0からは「.NET Core」として別系統で開発がされるようになりました。でも次期.NETのバージョンから.NET Coreは.NETに統合され、またよりを戻すことになりそうです、、、(2019年6月11日現在)。

とりあえず作ってみる

Visual Studio 2019のプロジェクトテンプレートから作成されるハロワ的なアプリケーションを作成し、そのコードの流れをご説明していきます。

では、早速作ってみます。Visual Studio 2019を起動して、「新しいプロジェクトの作成」をクリックします。

Screen Shot 2019-06-11 at 22.38.32

 

「ASP.NET Core Web アプリケーション」をクリックします。

Screen Shot 2019-06-11 at 22.38.40

 

プロジェクト名に任意の名前を入れて「作成(C)」をクリックします。

Screen Shot 2019-06-11 at 22.39.08

 

とりあえずデバッグ実行してみますと、、、

Screen Shot 2019-06-11 at 22.45.43

 

ブラウザが起動して、ハロワが表示されました。

Screen Shot 2019-06-11 at 22.46.07

起動の流れ

ソリューションエクスプローラーを見てみると、「Program.cs」「Startup.cs」という2つのコードが出来上がっています。この2つのコードがキモになります。以降はこのコードを元に、ASP.NET CoreによるWebアプリケーションの起動までの流れを追っていきます。

Screen Shot 2019-06-11 at 23.13.28

Program.cs

まずはProgram.csに書いてあるエントリポイントMainメソッドから始まります。

public class Program
{
    public static void Main(string[] args)
    {   
        CreateWebHostBuilder(args).Build().Run();
    }   

    public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
        WebHost.CreateDefaultBuilder(args)
            .UseStartup();
}

これは、Webアプリケーションを起動するために使用するWebサーバーの設定を定義します。どのWebサーバーを使うか、どのような設定ファイルを読み込むかといったような、Webサーバーの起動に必要な情報を定義します。上記の「CreateDefaultBuilder」というメソッドは、一般的なWebサーバーの定義をやってくれるのですが、本番用途でCreateDefaultBuilderメソッドで定義される初期設定を使うことはまずないと思います。

ということで、上記のコードを以下のように書き換えます。

public class Program
{
    public static void Main(string[] args)
    {   
        var host = new WebHostBuilder()
            .UseKestrel()
            .UseContentRoot(Directory.GetCurrentDirectory())
            .ConfigureAppConfiguration((hostingContext, config) =>
            {   
                var env = hostingContext.HostingEnvironment;

                config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
                      .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: true)
                      .AddEnvironmentVariables();
            })  
            .UseIISIntegration()
            .UseStartup<Startup>()
            .Build();

        host.Run();
    }   
}

このコードは、先程のCreateDefaultBuilder内部でやっていたことを、手動でやっています。上記はごくごく一般的な設定ですが、実際のプロジェクトではもうちょっと色々カスタマイズするのではないでしょうか?

では、上記のソースコードが何をやっているのかを一つずつ説明したいと思います。

var host = new WebHostBuilder()

まずは、WebHostBuilderのインスタンスを作っています。これにメソッドチェーンで次々と色々なメソッドをつなげていくことで、Webサーバーとしての設定を行います。

 

.UseKestrel()

KestrelというWebサーバーを使用することを表しています。KestrelはASP.NET Coreに標準で含まれているWebサーバーです。

 

.UseContentRoot(Directory.GetCurrentDirectory())

後述するappsettings.jsonなどの設定ファイル類を読みに行くディレクトリを設定します。上記の設定は、dotnetコマンド(ASP.NET Coreをビルドして出来たアプリを実行するコマンド)を実行したディレクトリを設定ファイル配置ディレクトリに設定しますよという設定になります。Visual Studioでデバッグ実行するときは、プロジェクトトップのディレクトリになるようです。

 

.ConfigureAppConfiguration((hostingContext, config) =>

後述するappsettings.jsonなどの設定ファイルや環境変数の設定を行うためのメソッドです。この関数の引数は、WebHostBuilderContext, IConfigurationBuilderの2つの引数、戻り値なしのdelegateになります。

 

var env = hostingContext.HostingEnvironment;

OSに設定された環境変数を取得します。

 

config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
    .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: true)

1行目はappsettings.jsonを設定ファイルとして読み込みますよという設定です。optionalはこのファイルが存在しなかったとしても例外としないという設定です。trueだと例外としません。reloadOnChangeは読んで字のごとく、このファイルに変更があったら自動で読み込み直す設定です。

2行目は、1行目との違いは読み込むファイルです。env.EnvironmentNameとありますが、これはOSの環境変数ASPNETCORE_ENVIRONMENTに設定された値になります。

例えばASPNETCORE_ENVIRONMENTにProductionが設定されていますと、ここで設定されるファイル名は、appsettings.production.jsonになります。環境変数にDevelopmentと大文字を混ぜても、実際に読み込まれるファイル名は、大文字小文字関係ないような動作をしていました。 そしてここではAddJsonFileする順番が重要になります。この順番通りにこれらの設定ファイルが読み込まれ、後から読み込まれたものが優先されます。

上記の設定では、まず最初にappsettings.jsonを読み込もうとします。読み込む規定のディレクトリは、UseContentRootメソッドで指定したように、dotnetコマンドを実行した直下のディレクトリ、Visual Studioでデバッグ時にはプロジェクトのトップのディレクトリになります。そして、optionalがtrueなので、appsettings.jsonが存在すれば、それを読み込み、なければ、次のAddJsonFileで定義したappsettings.production.jsonを読み込みます。

つまりこの場合、最初に読み込んだappsettings.jsonは無効になり、appsettings.production.jsonが有効になります。 環境変数のASPNETCORE_ENVIRONMENTで指定した値の設定ファイルを読み込みますので、例えば、本番環境用のappsettings.production.json、開発環境用のappsettings.development.jsonといったように環境ごとの複数のファイルを用意しておき、環境変数で設定を切り替えることができるわけですヮ(゚д゚)ォ!

 

.AddEnvironmentVariables();

OSで設定した環境変数を取得できるようにする設定です。

 

.UseIISIntegration()

IIS統合ということなのですが、、、ドキュメント読んでもよくわかりませんでした(´・ω・`)

 

.UseStartup<Startup>()

アプリケーション自体の設定をするためのクラスを指定します。この後に説明致します。

 

.Build();

今までメッソドチェーンで色々した設定を完了しますみたいなおまじないみたいなものでしょう。

 

host.Run();

Webアプリケーションを起動します。

Startup.cs

Program.csクラスのUseStartupメソッドのジェネリックで指定されていたクラスで、アプリケーションの動作を定義するクラスになります。ソースコードは以下のとおりです。

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
    }

    public void Configure(IApplicationBuilder app, IHostingEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }

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

では、ソースコードを細かくばらして、一つずつご説明していきたいと思います。

 

public void ConfigureServices(IServiceCollection services)
{
}

ASP.NET Coreでは標準でDI(Dependency Injection)をサポートしており、そのDIの定義をするためのメソッドになります。が、基本的なアプリケーションを動作させるのに、必ずしもDIは必須ではありませんし、ここでDIのことまで触れますと、話がややこしくなりそうなので、割愛させて頂きます(´・ω・`)

 

public void Configure(IApplicationBuilder app, IHostingEnvironment env)

ASP.NET Coreのランタイムから呼ばれるメソッドで、HTTPのリクエストパイプラインを設定します。HTTPのリクエストパイプラインについては、後ほど、詳細を説明致します。

 

if (env.IsDevelopment())
{
    app.UseDeveloperExceptionPage();
}

環境変数ASPNETCORE_ENVIRONMENTに設定された値がDevelopmentつまり開発環境だったら、より詳細なエラーメッセージを出力するという設定です。Developmentの場合はenv.IsDevelopmentメソッドで判定が出来ます。その他、APS.NET CoreではDevelopmentの他にもStaging、Productionという設定を標準でサポートしています。

  • Development:開発(env.IsDevelopmentで判定可能)
  • Staging:ステージング(env.IsStagingで判定可能)
  • Production:本番(env.IsProductionで判定可能)
app.Run(async (context) =>
{
    await context.Response.WriteAsync("Hello World!");
});

こちらがHTTPリクエストのパイプラインを作成しているところです。ASP.NET Coreでは、HTTPリクエストはパイプラインというもので処理されます。

パイプラインは複数のミドルウェアで構成されている、HTTPリクエストを処理する一連の流れであり、あらかじめ設定した順番通りに複数のミドルウェアを経由します。

と、なかなかテキストだけでは伝わりにくいので、ちょっと絵にしてみました。

 

Screen Shot 2019-06-15 at 8.45.55

上記の図がHTTPパイプラインリクエストのイメージです。複数のミドルウェア1〜3が順番にHTTPリクエストを処理して、最後にHTTPレスポンスをユーザーに返しています。

上図の構成では、ユーザーがブラウザなどから出力したHTTPリクエストは、以下の順番で処理されます。

  1. Middleware1の前処理で処理される。
  2. Middleware1のNextメソッドが呼ばれて、Middleware2に処理が移る。
  3. Middleware2の前処理で処理される。
  4. Middleware2のNextメソッドが呼ばれて、Middleware3に処理が移る。
  5. Middleware3で処理されて、Middleware2の後処理に移る。
  6. Middleware2で処理されて、Middleware1の後処理に移る。
  7. 最終的にユーザーにHTTPレスポンスが返る。

抽象的なお話ばかりだったので、具体的な実例と、それを実現するソースコードを作ってみたいと思います。

Screen Shot 2019-06-15 at 8.46.04

上図は、以下の要件を満たすミドルウェアになります。

  • HTTPリクエストの処理開始のログと処理終了のログを出力する。
  • x-hogeというHTTPリクエストヘッダーを追加する。
  • Hello World!!というHTTPレスポンスを生成する。

そして、上記のミドルウェアを実現するソースコードが以下になります。

app.Use(async (context, next) => {
    Console.WriteLine("HTTPリクエスト処理開始"); // (1)
    await next.Invoke(); // (2)
    Console.WriteLine("HTTPリクエスト処理終了"); // (3)
});

app.Use(async (context, next) => {
    context.Request.Headers.Add("x-hoge","fuga"); // (4)
    await next.Invoke(); // (5)
    Console.WriteLine("HTTPリクエストヘッダー追加処理終了"); // (6)
});

app.Run(async (context) => 
{
    await context.Response.WriteAsync("Hello World!"); // (7)
});

以降では、上記のソースを詳細に解説したいと思います。では、図中にあるMiddleware1に該当する部分から。

 

app.Use(async (context, next) => {
    Console.WriteLine("HTTPリクエスト処理開始");
    await next.Invoke();
    Console.WriteLine("HTTPリクエスト処理終了");
});

まず、パイプラインを構成する一番最初のミドルウェア(Middleware1)を作成します。appはConfigureメソッドの第一引数であり、IApplicationBuilderインターフェースを実装したインスタンスになります。これは、ASP.NET Coreの仕組みで、(おそらく)DIされてくるものであろうと推測します。

そして、Useメソッドは、次のミドルウェアに処理をつなげるためのものになります。Useメソッドは以下のDelegateを引数に取ります。

  • 第1引数:HTTPリクエスト及びHTTPレスポンスのContext
  • 第2引数:次に実施するミドルウェアを表したDelegateで戻り値はTask
  • 戻り値:Task

「Console.WriteLine(“HTTPリクエスト処理開始”);」の部分が処理開始のログを出力する部分に相当します。本来であれば、ILoggerインターフェースを使ったりするものですが、ここでは処理を簡略化するため、標準出力に出すだけとしています。最後に「Console.WriteLine(“HTTPリクエスト処理終了”);」として処理が終了した旨を表すログを出力しています。

そして、next.Invoke();では、次のミドルウェアを呼び出しています。ここでは、次に説明するMiddleware2を呼び出すこと同じ意味となります。

今までの説明からも分かる通り、ミドルウェアはUseメソッドもしくはRunメソッドで定義する順番がそのまま、HTTPリクエストパイプラインとして適用される順番になります。なので、今回の具体例のとおりに、Middleware1→Middleware2→Middleware3の順番で適用したいのであれば、その順番どおりにUseメソッドもしくはRunメソッドで定義していく必要があります。

次に、図中のMiddleware2に該当するソースコードの説明を致します。

 

app.Use(async (context, next) => {
    context.Request.Headers.Add("x-hoge","fuga");
    await next.Invoke();
    Console.WriteLine("HTTPリクエストヘッダー追加処理終了");
});

<p>&nbsp;</p>

図中にある通り、Middleware2では、HTTPリクエストヘッダーにx-hogeを追加しています。そして、Middleware1と同様にnext.Invoke();を実施して次のミドルウェア(Middleware3) を呼び出しています。

次に、図中のMiddleware3に該当するソースコードの説明を致します。

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

ここでは、Useメソッドではなく、Runメソッドを使用しています。RunメソッドはHTTPリクエスパイプラインを終端したい場合に利用します。つまりここでは、Middleware3以降で呼び出したいミドルウェアはありません。だからRunメソッドを使います。

context.Response.WriteAsyncメソッドは、引数で指定した文字列をHTTPレスポンスに出力するメソッドです。

ここで、全体のソースコードを見ていただきたいのですが、ミドルウェアの中で実施する処理に(1)〜(7)まで番号を振っております。今回構成したパイプラインでは、以下の順番で処理が実施されます。

(1)→(2)→(4)→(5)→(7)→(6)→(3)

ちなみにここからは私の推測です。このパイプラインという処理は、ASP.NET CoreによってIApplicationBuilderインターフェースの実装インスタンスが、Application ContextなスコープでDIされ、そのインスタンスに対してUseメソッドやRunメソッドを使うと、その引数で指定したDelegate(=ミドルウェア)が、IApplicationBuilderインターフェースの実装インスタンスのメンバー変数にどんどん追加されていきます。そして、HTTPリクエストが来るたびに、DIされたミドルウェアを呼び出して適用しているのではないかと推測しております。

まとめ

おそらく今後は、.NETでアプリケーションを開発する際は、ASP.NET Coreを使うことが主流となってくるのではと思います。次回はDIやカスタムミドルウェアあたりについて書きたいと思います。

>> 雑誌等の執筆依頼を受付しております。
   ご希望の方はお気軽にお問い合わせください!

ご覧いただきありがとうございます! この投稿はお役に立ちましたか?

役に立った 役に立たなかった

2人がこの投稿は役に立ったと言っています。

コメント投稿

メールアドレスは表示されません。


*