ASP.NET Core 是新一代的 ASP.NET,第一次出現時代号為 ASP.NET vNext,後來命名為ASP.NET 5,随着它的完善與成熟,最終命名為 ASP.NET Core,表明它不是 ASP.NET 的更新,而是一個重新設計的Web開發架構。而它一個非常重要的變化就是它不再依賴于IIS,而是一個獨立的自寄宿的控制台應用程式,這也是它可以跨平台的基石,而本文就來詳細探讨一下 ASP.NET Core 的啟動過程。
前言
我們先回顧一下以前的 ASP.NET 是怎麼來運作的:

ASP.NET 是嚴重依賴于IIS的,System.Web 中有很多方法都是直接調用的 IIS API,并且它還是駐留在IIS程序中的。而 ASP.NET Core 的運作則是一個完全獨立的控制台程式,它有自己的 Kestrel Server,可以直接對外部提供服務。
不過 Kestrel 的功能相對較于簡單,是以我們還是需要一個反向代理伺服器将 Kestrel 伺服器保護起來。而微軟也為我們提供了
UseIISIntegration
方法,友善與IIS進行內建。是以,在 Windows 下,通常還是使用IIS來部署,那麼,此時與 ASP.NET 的運作方式又有什麼差別呢?
通過上圖,可以很清楚的明白它們的差別。在 ASP.NET Core 中,IIS 是通過 HTTP 的方式來調用我們的 ASP.NET Core 程式。而部署在IIS中時,并不需要我們手動來啟動 ASP.NET Core 的控制台程式,這是因為IIS新增了一個 AspNetCoreModule 子產品,它負責 ASP.NET Core 程式的啟動與停止,并能監聽 ASP.NET Core 程式的狀态,在我們的應用程式意外崩潰時重新啟動。
下面開始進入正題,進入到 ASP.NET Core 的代碼中去。
WebHost的建立
對于一個程式控制台程式來說,它的入口點便是 Program 中的
Main
方法,ASP.NET Core 程式自然也不例外:
public class Program
{
public static void Main(string[] args)
{
BuildWebHost(args).Run();
}
public static IWebHost BuildWebHost(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>()
.Build();
}
WebHost.CreateDefaultBuilder
是在 2.0 中新增的,隻是用來簡化我們的代碼,其内部實作與 1.0 中的類似,主要做了6件事:
- 注冊 Kestrel 中間件,指定 WebHost 要使用的 Server(HTTP伺服器)。
- 設定 Content 根目錄,将目前項目的根目錄作為 ContentRoot 的目錄。
- 讀取 appsettinggs.json 配置檔案,開發環境下的 UserSecrets 以及環境變量和指令行參數。
- 讀取配置檔案中的 Logging 節點,對日志系統進行配置。
- 添加 IISIntegration 中間件。
- 設定開發環境下, ServiceProvider 的
為ValidateScopes
,避免直接在true
方法中擷取 Scope 執行個體。Configure
然後指定
Startup
類,最後通過
Build
方法建構出 WebHost,WebHostBuilder 的代碼較多,感興趣的可以去看完整代碼: WebHostBuilder,而我在這裡隻展示部分代碼片段來幫助了解:
public IWebHost Build()
{
var hostingServices = BuildCommonServices(out var hostingStartupErrors);
var applicationServices = hostingServices.Clone();
var hostingServiceProvider = hostingServices.BuildServiceProvider();
AddApplicationServices(applicationServices, hostingServiceProvider);
}
Build 中的
BuildCommonServices
方法主要有兩個功能:
首先在程式集中查找 HostingStartupAttribute:
if (!_options.PreventHostingStartup)
{
var exceptions = new List<Exception>();
// Execute the hosting startup assemblies
foreach (var assemblyName in _options.HostingStartupAssemblies)
{
var assembly = Assembly.Load(new AssemblyName(assemblyName));
foreach (var attribute in assembly.GetCustomAttributes<HostingStartupAttribute>())
{
var hostingStartup = (IHostingStartup)Activator.CreateInstance(attribute.HostingStartupType);
hostingStartup.Configure(this);
}
}
}
HostingStartupAttribute 給我們一個在其它程式集中做一些啟動配置的機會,在我們進行多層開發及子產品化的時候非常有用,後面會再詳細解釋。
然後便是查找我們的 Startup 類:
if (!string.IsNullOrEmpty(_options.StartupAssembly))
{
var startupType = StartupLoader.FindStartupType(_options.StartupAssembly, _hostingEnvironment.EnvironmentName);
if (typeof(IStartup).GetTypeInfo().IsAssignableFrom(startupType.GetTypeInfo()))
{
services.AddSingleton(typeof(IStartup), startupType);
}
else
{
services.AddSingleton(typeof(IStartup), sp =>
{
var hostingEnvironment = sp.GetRequiredService<IHostingEnvironment>();
var methods = StartupLoader.LoadMethods(sp, startupType, hostingEnvironment.EnvironmentName);
return new ConventionBasedStartup(methods);
});
}
}
首先是判斷是否有
_options.StartupAssembly
,對應配置檔案中的 "startupAssembly" ,如果我們沒有設定,那便是空的,并不會執行上面代碼。通常我們會使用
UseStartup<Startup>
的方法來注冊 Startup 類,而他們的作用是一樣的,都是将我們的 Startup 類做為一個單例注冊到了 DI 系統。
而最終
BuildCommonServices
傳回一個
IServiceCollection
,用于建構 hostingServiceProvider:
var hostingServices = BuildCommonServices(out var hostingStartupErrors);
var applicationServices = hostingServices.Clone();
var hostingServiceProvider = hostingServices.BuildServiceProvider();
接下來建立 WebHost :
public IWebHost Build()
{
var host = new WebHost(
applicationServices,
hostingServiceProvider,
_options,
_config,
hostingStartupErrors);
}
host.Initialize();
return host;
}
這裡需要說明的,hostingServiceProvider 是 ASP.NET Core 中的第一個 ServiceProvider,也是根 ServiceProvider,但它是在我們的 Starpup 類執行之前建立的,也就是說并不會包含我們在 ConfigureServices 中注冊的服務(但包含使用 HostingStartupAttribute 注冊的服務)。
WebHost啟動流程
在上一步,建立完 WebHost 之後,便調用它的 Run 方法,而 Run 方法再去調用 WebHost 的
StartAsync
方法,開始 ASP.NET Core 的啟動工作,主要包含以下幾個步驟:
1. 初始化,建構 RequestDelegate
RequestDelegate 是我們的應用程式處理請求,輸出響應的整個過程,也就是我們的 ASP.NET Core 請求管道。
而它有如下定義:
public delegate Task RequestDelegate(HttpContext context);
這裡不再對 RequestDelegate 進行過多的介紹,以後會詳細解釋。
1.1. 調用 Startup 中的 ConfigureServices 方法
在前面介紹過,我們的 Startup 類已經注冊到了 ASP.NET Coer 的 DI 系統中,是以可以直接從 DI 中擷取:
private IStartup _startup;
private IServiceProvider _applicationServices;
_startup = _hostingServiceProvider.GetRequiredService<IStartup>();
_applicationServices = _startup.ConfigureServices(_applicationServiceCollection);
這裡使用的
_hostingServiceProvider
是我們在 WebHost 中建立的根 ServieProvider。
1.2. 初始化 Http Server
Server 是一個HTTP伺服器,負責HTTP的監聽,接收一組 FeatureCollection 類型的原始請求,并将其包裝成 HttpContext 以供我們的應用程式完成響應的處理。
public interface IServer : IDisposable
{
IFeatureCollection Features { get; }
Task StartAsync<TContext>(IHttpApplication<TContext> application, CancellationToken cancellationToken);
Task StopAsync(CancellationToken cancellationToken);
}
而上面注冊的 Kestrel 便是預設的 Server :
public static IWebHostBuilder UseKestrel(this IWebHostBuilder hostBuilder)
{
hostBuilder.UseLibuv();
return hostBuilder.ConfigureServices(services =>
{
services.AddTransient<IConfigureOptions<KestrelServerOptions>, KestrelServerOptionsSetup>();
services.AddSingleton<IServer, KestrelServer>();
});
}
Server的初始化主要是配置要監聽的位址:
private void EnsureServer()
{
if (Server == null)
{
Server = _applicationServices.GetRequiredService<IServer>();
var serverAddressesFeature = Server.Features?.Get<IServerAddressesFeature>();
var addresses = serverAddressesFeature?.Addresses;
if (addresses != null && !addresses.IsReadOnly && addresses.Count == 0)
{
var urls = _config[WebHostDefaults.ServerUrlsKey] ?? _config[DeprecatedServerUrlsKey];
if (!string.IsNullOrEmpty(urls))
{
serverAddressesFeature.PreferHostingUrls = WebHostUtilities.ParseBool(_config, WebHostDefaults.PreferHostingUrlsKey);
foreach (var value in urls.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries))
{
addresses.Add(value);
}
}
}
}
}
Addresses 預設是通過在
launchSettings.json
中來查找的。
1.3. 建立 IApplicationBuilder
IApplicationBuilder 用于建構應用程式的請求管道,也就是生成 RequestDelegate,有如下定義:
public interface IApplicationBuilder
{
IServiceProvider ApplicationServices { get; set; }
IFeatureCollection ServerFeatures { get; }
IDictionary<string, object> Properties { get; }
RequestDelegate Build();
IApplicationBuilder New();
IApplicationBuilder Use(Func<RequestDelegate, RequestDelegate> middleware);
}
而它的建立過程是通過
ApplicationBuilderFactory
來建立的:
var builderFactory = _applicationServices.GetRequiredService<IApplicationBuilderFactory>();
var builder = builderFactory.CreateBuilder(Server.Features);
builder.ApplicationServices = _applicationServices;
IApplicationBuilderFactory 的預設實作 ApplicationBuilderFactory:
public IApplicationBuilder CreateBuilder(IFeatureCollection serverFeatures)
{
return new ApplicationBuilder(_serviceProvider, serverFeatures);
}
ApplicationBuilder 的實作方式就不在這裡多說了,在講中間件的時候再來細說。
1.4. 配置 IApplicationBuilder
我們比較的熟悉的是在 Startup 類的
Configure
方法中對 IApplicationBuilder 進行配置,其實還有一個 IStartupFilter 也可以用來配置 IApplicationBuilder,并且在 Startup 類的
Configure
方法之前執行:
var startupFilters = _applicationServices.GetService<IEnumerable<IStartupFilter>>();
Action<IApplicationBuilder> configure = _startup.Configure;
foreach (var filter in startupFilters.Reverse())
{
configure = filter.Configure(configure);
}
configure(builder);
然後調用 IApplicationBuilder 的
Build
方法,便完成了 RequestDelegate 的建立:
private RequestDelegate BuildApplication()
{
...
return builder.Build();
}
2. 啟動 Server,監聽請求并響應
Server 本身是并不清楚 HttpContext 的細節的,是以它需要接收一個 IHttpApplication 類型的參數,來負責 HttpContext 的建立,由如下定義:
public interface IHttpApplication<TContext>
{
TContext CreateContext(IFeatureCollection contextFeatures);
Task ProcessRequestAsync(TContext context);
void DisposeContext(TContext context, Exception exception);
}
它的預設實作是 HostingApplication 類,而 ProcessRequestAsync 方法則調用我們上面建立的 RequestDelegate 委托,來完成對 HttpContext 的處理:
public class HostingApplication : IHttpApplication<HostingApplication.Context>
{
private readonly RequestDelegate _application;
public Task ProcessRequestAsync(Context context)
{
return _application(context.HttpContext);
}
}
最後啟動 Server:
var httpContextFactory = _applicationServices.GetRequiredService<IHttpContextFactory>();
var hostingApp = new HostingApplication(_application, _logger, diagnosticSource, httpContextFactory);
await Server.StartAsync(hostingApp, cancellationToken).ConfigureAwait(false);
Server 會綁定一個監聽端口,注冊HTTP連接配接事件,最終交給
Http2Stream<TContext>
來處理,通過上面的 hostingApp 來切入到我們的應用程式中,完成整個請求的處理:
public class Http2Stream<TContext> : Http2Stream
{
private readonly IHttpApplication<TContext> _application;
public override async Task ProcessRequestAsync()
{
...
var context = _application.CreateContext(this);
try
{
await _application.ProcessRequestAsync(context);
...
}
finally
{
_application.DisposeContext(context, _applicationException);
...
}
...
}
}
3. 啟動 HostedService
HostedService 為我們提供一個注冊背景運作服務的機會,它會在随着我們的 ASP.NET Core 程式啟動而啟動,并在 ASP.NET Core 停止時進行優雅的關閉,有如下定義:
public interface IHostedService
{
Task StartAsync(CancellationToken cancellationToken);
Task StopAsync(CancellationToken cancellationToken);
}
而它是通過
HostedServiceExecutor
來執行的:
public class HostedServiceExecutor
{
private readonly IEnumerable<IHostedService> _services;
public async Task StartAsync(CancellationToken token)
{
await ExecuteAsync(service => service.StartAsync(token));
}
public async Task StopAsync(CancellationToken token)
{
await ExecuteAsync(service => service.StopAsync(token));
}
private async Task ExecuteAsync(Func<IHostedService, Task> callback)
{
foreach (var service in _services)
{
await callback(service);
}
}
}
WebHost 會調用 HostedServiceExecutor 的
StartAsync
,進而完成對 HostedService 的啟動:
_applicationLifetime = _applicationServices.GetRequiredService<IApplicationLifetime>() as ApplicationLifetime;
_hostedServiceExecutor = _applicationServices.GetRequiredService<HostedServiceExecutor>();
// Fire IApplicationLifetime.Started
_applicationLifetime?.NotifyStarted();
// Fire IHostedService.Start
await _hostedServiceExecutor.StartAsync(cancellationToken).ConfigureAwait(false);
這裡還有對 IApplicationLifetime 啟動事件的觸發,以後會介紹一下 IApplicationLifetime 的用途。
到此 WebHost 的整個啟動過程介紹完畢。
總結
本文粗略地介紹了一下 ASP.NET Core 中 WebHost 建立及啟動,它也是 ASP.NET Core 中的宿主,包含 HttpServer 的啟動與監聽,而其中也涉及到了很多關鍵點,對我們以後的開發非常有用,由于篇幅有限,下一章再來介紹一些本文沒有解釋清楚的概念。
參考文章:
Publishing-and-Running-ASPNET-Core-Applications-with-IIS