環境:
- window 10
- .netcore 3.1
- vs2019 16.5.1
- dnspy v6.1.4
說明:
上一篇介紹到了asp.net core中的通用主機,這個通用主機封裝了日誌、配置、依賴容器等資源,並且裏面包括了一系列的主機服務(IHostedService),而我們常用的web服務(GenericWebHostService:IHostedService)就是其中的一個主機服務。這篇就來分析,asp.net core框架是怎樣將web服務注入到通用主機中的,並且Startup是怎麼被調用的。
問題:
在講asp.net core中的web服務之前,先拋出了幾個問題:
- 什麼時候執行
webBuilder.UseStartup<Startup>();
- 什麼時候創建好依賴容器,什麼時候可以使用它,在Startup中可以使用它嗎?
- 什麼時候實例化的Startup,構造函數中可以獲取哪些對象?
- 什麼時候調用的Startup的ConfigureServices方法,方法參數中可以獲取哪些對象?
- 什麼時候調用的Startup的Configure方法,方法參數中可以獲取哪些對象?
一、web服務和Startup被調用的流程圖
在上一篇,我們用了一個流程圖來說明通用主機的構建和啓動過程,但僅僅討論了通用主機,這次我們把web服務加入進去:
二、關於Startup中的疑問
通過上圖中的描述,我們已經可以回答上面那些問題了:
- 什麼時候執行
webBuilder.UseStartup<Startup>();
?在進行主機配置的時候,即:在向主機構建者對象中加入構建邏輯的時候。
通用主機的運轉順序爲:創造構建者對象–>向構建者對象中加入構建邏輯–>構建者對象執行Build方法構建出主機–>啓動主機。 - 什麼時候創建好依賴容器,什麼時候可以使用它,在Startup中可以使用它嗎?
在主機Build完成後會創建好依賴容器,但Startup的實例化和調用ConfigureServices方法之前並沒有創建依賴容器,所以不能在Startup的構造參數和ConfigureServices方法參數中使用依賴容器(注:Startup中的參數不是從主機的依賴容器中獲取的,而是根據簡單的類型判斷提供的而且只能提供兩類對象,下面會有講到),但是在Configure方法中可以從依賴容器中獲取對象,因爲Startup中的Configure方法是在主機Run的時候被調用的,而此時依賴容器已經創建完畢。
- 什麼時候實例化的Startup,構造函數中可以獲取哪些對象?
在主機Build的時候會實例化Startup,因爲此時還沒有創建依賴容器所以不能從依賴容器中獲取對象,但是你仍然可以傳入兩類參數(這兩類參數提供的方式是簡單的if/else判斷,並沒有牽扯到依賴容器,事實上在依賴容器創建之前這兩類參數已經創建完成了,只不過還沒有放到依賴容器中而已),這兩類參數是:
1.環境: IHostingEnvironment、IWebHostEnvironment和IHostEnvironment其中的一種,無論你寫它們中的哪個,獲取到的都是同一個對象
2.配置: IConfiguration - 什麼時候調用的Startup的ConfigureServices方法,方法參數中可以獲取哪些對象?
在實例化Startup後會馬上調用ConfigureServices方法,方法參數最多隻能有IServiceCollection。
注:調用這個方法時,主機的依賴容器並沒有創建。 - 什麼時候調用的Startup的Configure方法,方法參數中可以獲取哪些對象?
在主機啓動(
Host.Run()
)時首先實例化Web服務(GenericWebHostService),然後再開啓web服務,而Configure方法是在實例化web服務的過程中調用的,所以在Configure方法中你可以從依賴容器中獲取需要的實例。
三、從源碼角度看web服務(GenericWebHostService)和Startup的執行順序
3.1 web服務是怎麼注入到通用主機的?
首先我們來看webapi項目生成代碼:
public class Program
{
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});
}
在上面代碼中,我們通過.ConfigureWebHostDefaults(...)
將web服務注入進去的,其他的代碼都是針對通用主機的,我們不管,調試進入ConfigureWebHostDefaults方法:
通過上圖可以看出,我們創建了一個web主機服務的構造者對象(GenericWebHostBuilder),而這個 GenericWebHostBuilder其實就是封裝了通用主機構造者對象(HostBuilder) 而已,那麼通過GenericWebHostBuilder注入的構建邏輯也就存儲到了HostBuilder的_configureHostConfigActions 、_configureAppConfigActions 或_configureServicesActions 中了。然後我們向HostBuilder中注入了依賴服務配置邏輯(在這個邏輯中我們將web服務GenericWebHostService
添加到依賴容器中)。我們看一下GenericWebHostBuilder的構造函數:
通過上圖,我們應該對“ GenericWebHostBuilder其實就是封裝了通用主機構造者對象(HostBuilder) ”這句話有了直觀的理解了。
我們將上面調試代碼的執行邏輯梳理一下:
從上面圖可以看到,我們在通用主機配置的時候就執行了webBuilder.UseStartup<Startup>();
方法,那麼我們深入這個方法看一下:
可以看到上面的代碼執行過程,裏面的hostBuilder其實是:GenericWebHostBuilder。
代碼hostBuilder.UseSetting(WebHostDefaults.ApplicationKey, name);
的作用是將應用名稱存儲到GenericWebHostBuilder的配置中去。
後面的代碼supportsStartup.UseStartup(startupType)
是關鍵地方,我們調試進去:
到了這裏我們就知道了:webBuilder.UseStartup<Startup>();
就是將Startup的Type值存儲到主機構建者屬性裏面,並且將Startup的調用邏輯注入到了主機的Build過程中(注入到邏輯容器:_configureServicesActions),至此,我們應該明白了web服務以及Startup是怎麼注入到通用主機中的了。
3.2 Startup是怎麼被實例化的?
接着上面的代碼調試,我們進入HostBuilder的Build()方法:
可以看到,在創建依賴容器之前,代碼調用了一系列的依賴容器構建邏輯,而這些邏輯中就有上面注入的邏輯(Startup的實例化和ConfigureServices方法調用),那麼讓我們轉到這個處理邏輯:
從上圖中看到了Startup的創建和調用,接下來我們先看看Startup具體是怎麼構建的,首先看代碼:
instance = ActivatorUtilities.CreateInstance(new GenericWebHostBuilder.HostServiceProvider(webHostBuilderContext), startupType, Array.Empty<object>());
這裏的HostServiceProvider是一個內部類,它繼承自IServiceProvider,它的代碼如下:
可以看到,這個HostServiceProvider
僅提供了主機環境和配置,其他的不提供(因爲到現在爲止主機的依賴容器還未創建完成),所以我們在Startup的構造函數中的寫法:
public Startup(IConfiguration configuration, IWebHostEnvironment webHostEnvironment)
{
Configuration = configuration;
WebHostEnvironment = webHostEnvironment;
}
public IConfiguration Configuration { get; }
public IWebHostEnvironment WebHostEnvironment { get; }
因爲此時主機的依賴容器並沒有創建完成,所以你能在構造函數中獲取到的只有:
1.環境: IHostingEnvironment、IWebHostEnvironment、IHostEnvironment,無論你寫它們中的哪個,獲取到的都是同一個對象
2.配置: IConfiguration
下面的寫法是不正確的:
3.3 Startup的ConfigureServices方法是怎麼被調用的?
接着上面,我們再看看ConfigureServices方法的調用,它的調用比較直接,看源碼:
上圖中顯示:在調用ConfigureServices的時候直接傳入了一個參數IServiceCollection services
,所以我們實際調用的時候最多隻能傳入這個參數,傳入其他的參數肯定報錯(不傳入也是可以的,調用代碼時已經做了處理):
那麼通過上面的分析,我們知道了Startup的創建和ConfigureServices方法的調用都是在主機Build過程中的,那麼Startup的Configure方法是什麼時候被調用的呢?
3.4 Startuo的Configure方法是什麼時候被調用的?
還記得上面分析webBuilder.UseStartup<Startup>();
的時候我們看到了怎樣實例化Startup以及怎樣調用ConfigureServices方法的嗎,其實那段代碼下面就是處理Configure方法的:
從上圖中看出,在Host的Build過程中將Startup的Configure方法的調用配置到了web服務上的配置選項上(GenericWebHostServiceOptions),那麼我們來觀察下web服務是怎樣被創建的,這個Configure又是怎麼被調用的?
這個時候我們需要調試到Host的Run方法了,一路跟下去:
從上面看到在Host執行Run的時候IHostedService被依賴容器創建,那麼直接看它的構造函數(GenericWebHostService):
在上面的構造函數中,代碼this.Options = options.Value;
將會觸發創建GenericWebHostServiceOptions實例,進而觸發之前的配置,如下圖:
進而執行到configureBuilder.Build(instance)(app);
,進而調用了Startup的Configure方法。
注意: Configure被調用時主機的依賴容器已經創建完成,所以可以在Configure方法裏向依賴容器獲取數據。