ASP.NET Core RC2でHelloWorld~EntryPointとStartupも探る

ASP.NET Core RC2 と Visual Studio 2015 UPDATE 2でHello Worldを作ってみようと思います。 で、その後でASP.NET Coreの基本動作の部分について少し探索してみようかと思います。

まずは準備

Visual Studio 2015:UPDATE 2を適用した環境を用意

②.NET Core RC2をインストール

https://www.microsoft.com/net」にアクセスして.「Download NET Core」をクリック

f:id:daigo-knowlbo:20160529030307p:plain

Visual Studio official MSI Installer」をクリックしてダウンロードしたインストーラを実行。

「NuGet Manager extension for Visual Studio」をクリックしてダウンロードしたインストーラを実行。

f:id:daigo-knowlbo:20160529030415p:plain

これでASP.NET Core RC2の開発を行う環境が整いました!

Hello Worldの作成

Visual Studio 2015を起動し「ファイル→新規作成→プロジェクト」メニューを選択

f:id:daigo-knowlbo:20160529030822p:plain

テンプレート:Web→ASP.NET Core Web Application(.NET Core) 名前:HelloAspNetCore

とします。

f:id:daigo-knowlbo:20160529031039p:plain

空を選択します。

以下のようなソリューション・プロジェクトが作成されました。

f:id:daigo-knowlbo:20160529031333p:plain

②実行

Hello World!が完成しました。

f:id:daigo-knowlbo:20160529031504p:plain

実装の確認

例にもれず最近の開発環境は自動生成コードでHello World(もしくはもっとリッチ)な実装を生成してくれます。 と、いうことでざっくり実装を見ていこうと思います。

ポイントは・・・ Program.csに実装されたエントリーポイント Startup.cs実装されたStartupクラス

エントリーポイント

従来のASP.NETのエントリーポイントは不明確(?)でありましたが、ASP.NET Coreでは、コンソールアプリケーションと同様に明確です。この点については、新人教育等で論理的にプログラムの動きを説明するにはASP.NET Coreの方が分かりやすいでしょう。

従来のコンソールアプリケーションと同様に「staticなMain()メソッド」がエントリーポイントとなります。 例えば、プロジェクトにClass.csを追加して、ここにも同様にpublic static void Main(string[] arg)メソッドを追加すると以下のようなビルドエラーとなります。複数のエントリーポイントがありますよ!と、/mainでエントリーポイントを含むタイプを指定しろ、と。まあ、通常複数のMain()を定義することはありませんね。

f:id:daigo-knowlbo:20160529132029p:plain

そしてProgram.csの実装が以下になります。

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Hosting;

namespace HelloAspNetCore
{
    public class Program
    {
        public static void Main(string[] args)
        {
            var host = new WebHostBuilder()
                .UseKestrel()
                .UseContentRoot(Directory.GetCurrentDirectory())
                .UseIISIntegration()
                .UseStartup<Startup>()
                .Build();

            host.Run();
        }
    }
}

WebHostBuilderクラスを利用し「このWebアプリケーションをホストする構成」を生成します。host変数は「IWebHostインターフェイス型」になります。 WebHostBuilderクラスの生成と同時にいくつかのメソッドを呼び出していますが、これらによって必要な構成を決定します。

  • UseKestrel()
    Kestrelと呼ばれるアプリケーションサーバーを使用することを示します。ASP.NET Coreはマルチプラットフォーム対応であるため、IISなどのWebサーバーとアプリケーションサーバーを分離した構成をとります。IIS等のWebサーバーはリバースプロキシの役割を果たし、Kestrelのようなアプリケーションサーバーでロジックが動作します。

  • UseContentRoot(Directory.GetCurrentDirectory())
    コンテンツルートをカレントディレクトリとします。

  • UseIISIntegration()
    IIS統合で動作する構成とします。

  • UseStartup()
    スタートアップ処理を実装したクラスとして「Startup」(厳密にはこのプロジェクトに実装されたHelloAspNetCore.Startupクラス)を使用するよう構成します。

上記3つのメソッドはWebHostBuilderの拡張メソッドであり、IWebHostBuilder型オブジェクトを返却します。 例えばUseKestrel()はMicrosoft.AspNetCore.Server.Kestrelアセンブリに、UseIISIntegration()はMicrosoft.AspNetCore.Server.IISIntegrationアセンブリに実装されています。

  • Build()
    設定した構成をビルドします。戻り値の型はIWebHostとなります。

そしてRun()の呼び出しで、設定した構成のWebホストを実行します。

Startupクラス

実装は以下の通りです。

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;

namespace HelloAspNetCore
{
  public class Startup
  {
    // This method gets called by the runtime. Use this method to add services to the container.
    // For more information on how to configure your application, visit http://go.microsoft.com/fwlink/?LinkID=398940
    public void ConfigureServices(IServiceCollection services)
    {
    }

    // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
    public void Configure(IApplicationBuilder app)
    {
      app.Run(async (context) =>
      {
        await context.Response.WriteAsync("Hello World!");
      });
    }
  }
}

Main()メソッド内で「UseStartup()」という構成を行いました。 これによって「スタートアップ時に使用するクラスはStartupクラスですよ」と設定したことになります。 そして、Build()(IWebHostBuilder.Build())内の処理においてスタートアップ処理が実行されます。 スタートアップクラスは以下のメソッドを実装しているものとしてルール化が図られており、これらのメソッドが呼び出されます。

  • public void ConfigureServices(IServiceCollection services)
  • public void Configure(IApplicationBuilder app)

ConfigureServices()は、サービスの構成の初期化になります。 例えば、ASP.NET CoreはDIをベースとしていますが、コントローラークラスから使用するサービスクラスのDI登録等を行うことになります。

Configure()は、HTTPリクエストパイプラインの構成を実装します。 自動生成されたコードでは、何がリクエストされても常にHello World!の文字をレスポンスする実装となっています。 つまり「http://localhost:24912/」とリクエストしても「http://localhost:24912/こんばんは」とリクエストしても常に「Hello World!」とブラウザには表示されます。

ConfigureServices() / Configure()とEnvironmentName

スタートアップクラスのASP.NET Coreのランタイム内でConfigureServices()とConfigure()が呼びだされますが少し不思議ですよね。特定のインターフェイスを実装したクラスでもありません。つまり、ランタイム内部ではUseStartupで指定されたTypeのクラスに対して、動的にメソッドを呼び出しているのでしょう。
というか実際にオープンソースの「Microsoft.AspNetCore.Hosting\Internal\StartupLoader.cs」あたりを覗くとMethodInfoを使ったリフレクションで処理が記述されているのが分かります。
さらにStartupLoader.csを除いていると以下のような実装が・・・

private static ConfigureBuilder FindConfigureDelegate(Type startupType, string environmentName)
{
  var configureMethod = FindMethod(startupType, "Configure{0}", environmentName, typeof(void), required: true);
  return new ConfigureBuilder(configureMethod);
}

private static ConfigureServicesBuilder FindConfigureServicesDelegate(Type startupType, string environmentName)
{
  var servicesMethod = FindMethod(startupType, "Configure{0}Services", environmentName, typeof(IServiceProvider), required: false)
    ?? FindMethod(startupType, "Configure{0}Services", environmentName, typeof(void), required: false);
  return servicesMethod == null ? null : new ConfigureServicesBuilder(servicesMethod);
}

ConfigureServices() / Configure()の両メソッドの名称を「Configure{0}, environmentName」「Configure{0}Service, environmentName」としています。

ということで、Startup.csの実装を以下のように修正してみましょう。 ConfigureDevelopmentServices()とConfigureDevelopment()を追加しました。

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;

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

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

    // >> 追加
    public void ConfigureDevelopmentServices(IServiceCollection services)
    {
    }

    public void ConfigureDevelopment(IApplicationBuilder app)
    {
      app.Run(async (context) =>
      {
        await context.Response.WriteAsync("Hello World!(Development Environment)");
      });
    }
    // << 追加
  }
}

また、environmentName(環境変数)とはVS2015でプロジェクトのプロパティの以下の値を表します。 「Development」となっています。

f:id:daigo-knowlbo:20160529145421p:plain

HelloAspNetCoreを実行してみましょう!

f:id:daigo-knowlbo:20160529145552p:plain

はい。Startup.ConfigureDevelopment()が呼び出されたことが分かります。

ちなみに、環境変数「ASPNETCORE_ENVIRONMENT」を削除して実行するとStartup.Configure()が呼び出される動作に戻ります。

f:id:daigo-knowlbo:20160529150227p:plain f:id:daigo-knowlbo:20160529031504p:plain