ASP.NET Core 的啟動方式 (Hosting)

  • 3842
  • 0
  • 2017-11-09

這一篇文章將討論 ASP.NET Core 的啟動方式以及啟動時所會執行的基本程式碼.

[2016.03.27] 更正 IIS http platform handler

[2016.04.06] 新的 extension method - UseKestrel()

這一篇文章應該要比  Middleware 先介紹,這樣的順序會比較好一點.

過去與現在

之前版本的 ASP.NET 必需依靠 IIS 來啟動,IIS 上會註冊 ASP.NET 所用的 ISAPI filter,當 http 連線進來時 IIS 會啟動 w3wp 的 worker process 來開始整個 ASP.NET  runtime 的程序.相信這樣子的方式是大家都知道的運作方式.之前版本的 ASP.NET 只能在 Windows 上執行,而且也沒有 .Net Core 的支援,所以 IIS 便是 Microsoft 產品裡唯一的 web server 選擇.然而,新版的 ASP.NET Core 有了 .Net Core 的支援可以進行跨平台,因此啟動的方式也得重新設計了.

Kestrel 與 IIS platform handler

在 ASP.NET Core 中,因為整個 runtime 都是重寫的,所以跟 IIS 之間的關係也有些改變.現在 ASP.NET Core 為了跨平台,所以它的執行方式就可以像一般的 Console app 一樣.ASP.NET Core 本身內建了一個高效能的 I/O 伺服器 - Kestrel,它可以不需要 IIS 的存在就啟動 runtime,然而 Kestel 是一個 I/O 元件,並沒有像 IIS 提供其他的功能來保護與管理,所以 ASP.NET Core 也可以透過 IIS 來進行存取.因此,如果要透過 IIS 來進行存取,則需要有一個中間人,而它的名字叫 Http Platform Handler,主是是設定於 web.config 檔中讓 IIS 進行讀取,裡面包含了啟動 ASP.NET Core 程式的路徑與檔名,需要傳遞的參數以及一些其他的設定選項.它的設定例子如下:

  <system.webServer>
    <handlers>
      <add name="httpPlatformHandler" path="*" verb="*" modules="httpPlatformHandler" resourceType="Unspecified"/>
    </handlers>
    <httpPlatform processPath="WebApp.exe" arguments="" stdoutLogEnabled="false" startupTimeLimit="3600"/>
  </system.webServer>

有關 IIS Http platform handler 請參考 http://www.iis.net/downloads/microsoft/httpplatformhandler

從上面的例子,你可以看到 ASP.NET Core 編譯完成後,主要程式就是一個 exe 檔案,讓你可以直接執行.因此 http 連線進來時,IIS 先接受,然後依照 web.config 的內容將連線轉給 WebApp.exe (若它未啟動,則先執行它),然後 WebApp.exe 一開始執行時便會先啟動 Kestrel,接著這個 http 連線就會進入 ASP.NET Core 的 runtime 世界裡.這樣的情況讓 IIS 的角色就像是一個 proxy/forwarder.

另外,在不久的未來 上面範例中的 <httpPlatform> 將會改成新的名字,前陣子 web deploy 團隊已經通知要做變更名稱.新的名稱是 <aspNetCore>,等未來你用 Visual Studio 2015 Update2 時,編輯器出現一個底線告訴你它看不懂這是什麼.這個 bug 剛好是我的,因為 web deploy 團隊通知的時間已經趕不上 Update2 的時間,所以這個 bug 還沒修正,因此你仍繼續看到有底線的警告.你可以忽略它,它不會影響它該有的功能,這項 bug 將在 Visual Studio 2015 Update3 更正.

[更正] 我剛剛才找到 email, aspNetCore 不只是改名字而己,它是一個新的 IIS module 用來取代 http platform handler. 可以參考這網址得到更詳細的說明 https://github.com/aspnet/IISIntegration/issues/105  

Main()

ASP.NET Core 和其他的 .Net 程式一樣都有一個 static void Main(),這是整個 runtime 的進入點.以下是某個範例

        public static void Main(string[] args)
        {
            var host = new WebHostBuilder()
                .UseServer("Microsoft.AspNetCore.Server.Kestrel")
                .UseContentRoot(Directory.GetCurrentDirectory())
                .UseDefaultConfiguration(args)
                .UseIISPlatformHandlerUrl()
                .UseStartup<Startup>()
                .Build();

            host.Run();
        }

ASP.NET Core host engine 的建立者是 WebHostBuilder.它實做了 IWebHostBuilder interface,其中 UseServer() 是用來指定要用什麼 server,其中 UseServer() 有一個 extension method 可以直接輸入它的 assembly name,所以 Kestrel 的 assembly name 就在此直接傳入.這只是其中一個選項.你也可以自己實做屬於自己的 server,只要你的 server 能實做 IServerFactory interface 即可,所以這樣的設計提供了一個很大的彈性讓你自行選擇 hosting server.

[2016-04-06] 可以使用新的 extension method - UseKestrel() , 詳情請參考 https://github.com/aspnet/KestrelHttpServer/pull/715

UseContentRoot() - 這一個 extension method 是讓你指定應用程式的 working directory,如果你沒有指定,則將以主程式 (webapp.exe) 所在目錄為 working directory.

UseDefaultConfiguration() - 這個 extension method 在 IWebHostBuilder 建立時提供一些參數有預設值的存在,比如 application key, environment name, server factory location, content root path 等等.因此,當你在執行 WebApp.exe 時,就可以在執行的時候把你需要用的 hosting 參數帶進來,這些參數也可以寫在 appsettings.json 裡然後再透過 Configuration 再被讀取.所以,UseDefaultConfiguration() 不見得要在這時候存在於 Main() 之中.如果我沒記錯的話,在寫這篇文章的時候,UseDefaultConfiguration() 會被改變名稱,改為 UseDefaultHostingConfiguration().改成這個新名稱顯然是更能清楚明白.

UseIISPlatofmrHandleUrl() - 這一個 IWebHostBuilder 的 extension method 比較特別,如果你要把 ASP.NET Core 放在 IIS 下,這一個 extension method 會讀取 IIS http platform handler 的 server port 和 application path,這將用來做為 ASP.NET Core 的啟動位置,如 http://localhost:5000/start.如果你沒用 IIS,那這個 method 便是多餘的.

UseStartup<>() - 這是 WebHostBuilder 裡相當重要的一個 extension method,它的 method signature 如下

public static IWebHostBuilder UseStartup<TStartup>(this IWebHostBuilder hostBuilder) where TStartup : class

你可以很清楚地看到 <> 裡面要放的就是一個 class,在這裡的範例中,它的名字是 Startup,裡面最重要的就是需要定義要使用那些 service 以及要使用那些 middleware,

Startup

這是一個相當重要的 class.在 ASP.NET Core 範本中都將它命名為 Startup,其實你要取其他的名稱也行,或是設定多個 Startup 也行.上面的內容中你看到了 UseStartup() 指定了誰是 startup class,然後在 Build() 就會 instantiate Startup class 且執行裡面兩個重要的 methods - ConfigureServices() 和 Configure().我們先來看 Startup 的 constructor.

        public Startup(IHostingEnvironment env)
        {
            // Set up configuration sources.
            var builder = new ConfigurationBuilder()
                .AddJsonFile("appsettings.json")
                .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true)
                .AddEnvironmentVariables();
            Configuration = builder.Build().ReloadOnChanged("appsettings.json");
        }

Host engine 在被執行 Build()時已經知道 startup type 是 Startup class,所以在 Build() 的時候會先建立其物件.在範本中選的是具有傳入 IHostingEnvironment 的 constructor,在上面的程式碼範例中,IHostingEnvironment 為我們帶來 EnvironmentName.在這個 constructor 裡,主要是建立出 Configuration 物件,這是一個蠻重要的基礎建設物件,以後的文章會來說明它.特別要說明的是,在上面的程式碼範例中,你可以看到執行了兩次 AddJsonFile(),而且第二個 json file 的檔名不太一樣.為什麼要這樣做呢 ? 這樣做的目的是讓開發者可以把開發環境用的環境參數和其他環境用的參數有所區別.比如,你的開發環境用的是 appsettings.json,這個檔案只存在於你的電腦,另一個檔案是 appsettings.production.json,這是正式環境用的參數設定檔,它只指在於 production server 上.在大部份的情況下,開發環境用的參數和正式環境用的參數應該會不同,如 database connection string.所以,開發環境用的參數只存在於你的電腦,由於你的電腦不會有 appsettings.production.json,所以 Configuration 物件裡面都是開發環境用的參數.因此,你可以看到第二個 AddJsonFile() 第二個參數是 true,也就是可能不存在的意思.當你在做 deploy 程式到正式環境時,你也把你的 appsettings.json 也上傳上去,但由於正式環境中存在 appsettings.production.json,所以若遇到重覆名稱的參數時,appsettings.production.json 會覆寫 appsettings.json 的內容.因此,這樣的設計方式也可以防止開發者知道正式環境的參數內容,讓分工可以更細一點,責任也可以劃分更明確.以上都是假設 EnvironmentName = "production".

接下來,在 IWebHostBuilder 的 Build() 裡會執行 host engine 初始化的程序,其中就會去找 Startup class 裡面兩個 methods - ConfigureServices() 和 Configure(). ConfigureSerivces() 是定義了這個 web application 要使用那些服務,然後將這些服務放在 service container (IServiceCollection) 裡面,如下例

        public void ConfigureServices(IServiceCollection services)
        {
            // add entity framework
            services.AddEntityFramework()
                    .AddDbContext<BlogsContext>(o => o.UseSqlServer(Configuration["Data1:DefaultConnection:ConnectionString"]))
             
            // Add framework services.
            services.AddMvc();
        }

它定義了 entity framework 和 mvc 兩個  services.這裡所謂的 services 的意思也就是透過它們再帶入更龐大的程式碼.這聽起來好像有點好笑,但也真的是如此.像 Entity framework 裡面有那麼多的程式碼,一定都需要帶入許多定義好的物件或是參數,而不是只有一個程式進入點而己,所以 services 的目的就在這裡.

Configure() 主要是定義了 middleware 以及它們的順序.

        public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
        {
            loggerFactory.AddConsole(Configuration.GetSection("Logging"));
            loggerFactory.AddDebug();

            app.UseStaticFiles();

            app.UseMvcWithDefaultRoute();
        }

Middleware 已經在上一篇文章中討論過了,所以在此先跳過.

如果你是個 ASP.NET 愛好者,若你有強大的興趣的話,你可以到 Github 上去看這兩個 methods 如何被呼叫的,在看之前先對 action 和 function delegate 有了解才能幫助你看懂.

Build 和 Run

最後,在 IHostWebBuilder 裡最後的兩個動作 - Build and Run. 

Build() - 這個 method 做的工作有建立 hosting service,把 Startup 中定義的 services 和 middleware 接收過來,然後確定 content root path 與 application name,接著依照前面這些資料再加上 Configuration 過來的資料來啟始化 host engine (WebHost.cs).

Run() - 這是啟動 host engine 的 extension method,它在啟動之前,它加入了一個 CancelKeyPress 的事件.因為在 Run() method 中傳入了 CancellationTokenSource() ,讓我們有一個方式可以隨時中斷 host engine 的執行.目前的做法就用是 CancelKeyPress 事件,所以你可以按下 Ctrl+C  來中止 host engine 的執行.比較特別的是,這一段中止的文字說明居然是用 hard code 到程式碼,參考如下:

host.Run(cts.Token, "Application started. Press Ctrl+C to shut down.");

看來目前似乎沒有將它多國語言化的打算喔!

以上就是整個 ASP.NET Core 的啟動方式,從程式的進入點 Main() 一直到 host engine Run().希望這樣的說明能讓你對 ASP.NET Core 的啟動能有更多的了解.

另外,你可能會發現我寫的內容和 docs.asp.net 上的說明有些出入.因為 docs.asp.net 的內容還沒更新上來,所以並不是那些作者寫錯了,只是文件更新的速度不如改 code 來的快.我目前寫的東西基本上是以 RC2 版本為主,希望到 RTM 版時也能適用.