ASP.NET Core 終於將幾乎所有的物件建立工作都和依賴註入框架集成了起來。並對大部分的日常工作進行了抽象。使得整個框架擴充套件更加方便。各個部分的整合也更加容易。今天我們要思考的部分仍然是從一段每一個工程中都大同小異的程式碼開始的。
IWebHostBuilder CreateWebHostBuilder(string[] args)
{
return new WebHostBuilder()
.UseKestrel(ko => ko.AddServerHeader = false)
.ConfigureAppConfiguration(cb => cb.AddCommandLine(args))
.ConfigureLogging(lb => {...})
.UseStartup();
}
0 太長不讀
- ASP.NET Core 的初始化包含了兩個步驟:第一個步驟是 Hosting 相關服務的初始化過程,初始化完畢之後建立了第一個
IServiceProvider
物件;第二步是 Application 相關服務的初始化過程。而 Application 的初始化過程可以註入 Hosting 相關的服務。之後,透過IStartup.ConfigureServices
方法建立了第二個IServiceProvider
物件。 - 初始化過程中建立的兩個
IServiceProvider
均會跟隨WebHost
的銷毀而銷毀。 - 透過
Startup
型別的建構式註入的實體是由 Hosting 初始化階段建立的IServiceProvider
建立的。只能註入 Hosting 初始化階段新增的型別。且最好不要使用大量消耗資源的型別。 - 可以在
Startup.Configure
方法中新增其他引數,這樣會使用 Application 的一個Scope
下的IServiceProvider
進行註入,且在方法呼叫完畢之後該Scope
即被銷毀。因此該方法內可以建立資源佔用量較高的需要Dispose
的型別實體而不造成洩露。
1 WebHost 的構建主要就是向 `IServiceCollection` 中新增服務
之前提到過,任何 Framework 只有兩件事情,第一件事情就是物件怎麼建立,第二件事情就是如何將這些創建出來的物件塞到 Framework 處理流水線中。因此 ASP.NET Core 也是這樣。在應用程式啟動的時候,我們會在 WebHostBuilder.Build
方法呼叫之前進行各種各樣的操作,雖然我們呼叫的大部分操作都是擴充套件方法(例如上述程式碼中的 UseXxx
,和 ConfigureLogging
),但是歸根結底會呼叫 IWebHostBuilder
的以下方法:
IWebHostBuilder ConfigureAppConfiguration(Action configureDelegate);
IWebHostBuilder ConfigureServices(Action configureServices);
IWebHostBuilder ConfigureServices(Action configureServices);
不論調哪一個方法,它們做的事情其實都是一件。就是告訴應用程式,我到底有哪些物件需要建立,如何建立這些物件,以及其生存期如何管理。從技術角度上來說,就是將需要建立的物件型別新增到 IServiceCollection
中。如果感興趣的同學可以看看 WebHostBuilder
的實現程式碼(https://github.com/aspnet/AspNetCore/blob/master/src/Hosting/Hosting/src/WebHostBuilder.cs),就更加清晰了。
例如,以 ConfigureLogging
為例,程式碼請參見這裡(https://github.com/aspnet/Extensions/blob/master/src/Logging/Logging/src/LoggingServiceCollectionExtensions.cs):
public static IWebHostBuilder ConfigureLogging(
this IWebHostBuilder hostBuilder, Action ILoggingBuilder> configureLogging)
{
return hostBuilder.ConfigureServices((context, collection) =>
collection.AddLogging(builder => configureLogging(context, builder)));
}
public static IServiceCollection AddLogging(
this IServiceCollection services,
Action configure)
{
if (services == null) { throw new ArgumentNullException(nameof(services)); }
services.AddOptions();
services.TryAdd(ServiceDescriptor.Singleton());
services.TryAdd(ServiceDescriptor.Singleton(typeof(ILogger<>), typeof(Logger<>)));
services.TryAddEnumerable(ServiceDescriptor.Singleton>(
new DefaultLoggerLevelConfigureOptions(LogLevel.Information)));
configure(new LoggingBuilder(services));
return services;
}
可以看到實際上就是將 IOptions<>
、IOptionsSnapshot<>
、IOptionsMonitor<>
、IOptionsFactory<>
、IOptionsMonitorCache<>
以及 ILoggerFactory
、ILogger<>
、IConfigureOptions
新增到 IServiceCollection
中的過程。有關日誌的內容我們會在另一篇文章中介紹。
2 Startup 初始化時為什麼又能註入又有 `IServiceCollection` 呢
在 WebHost
的構建過程中,十有八九會出現 UseStartup
這句話(如果不出現這句話,那麼很大程度上使用了 Configure
擴充套件方法)。Startup
是整個 Web 應用程式的起點。應用程式(Web App)託管在宿主(Hosting Environment)中。那麼它應當是在初始化的最終階段執行的。我們來觀察一下它的典型結構:
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
// Add application related services to service collection.
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
// Create application pipeline. We will not focus on this method.
}
}
如果單純觀察上述程式碼那麼並沒有任何的稀奇之處。ConfigureServices
方法將應用需要的型別全部新增到 IServiceCollection
實體中,而 Configure
來構建 Pipeline(我們此次不討論該方法)。但是如果我們需要記錄日誌,讀取配置檔案,在應用程式生命週期事件中註冊新的處理方法時,我們可以將其直接註入 Startup
中。例如:
public class Startup
{
readonly IConfiguration configuration;
readonly IApplicationLifetime lifetime;
readonly ILogger logger;
public Startup(
IConfiguration configuration, IApplicationLifetime lifetime, ILogger logger)
{
this.configuration = configuration;
this.lifetime = lifetime;
this.logger = logger;
}
public void ConfigureServices(IServiceCollection services)
{
// Add application related services to service collection.
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
// Create application pipeline.
}
}
那麼問題就來了。
- 在
Startup
中註入的configuration
、lifetime
、logger
這些服務是由哪一個IServiceProvider
創建出來的呢? - 如果在
Startup
建立時IServiceProvider
已然建立,那麼Startup.ConfigureServices
在向哪個IServiceCollection
實體新增型別呢? - 應用程式執行期間的
IServiceProvider
是在Startup
建立之前就建立好的那個呢、還是由Startup
配置的IServiceCollection
實體建立的那個呢?
3 兩階段 ServiceProvider 建立
既然 Startup
中已經有一個 IServiceProvider
來給相應的型別進行依賴註入,而平時的應用程式中的依賴註入又能夠包含 Startup.ConfigureServices
中的型別定義,那麼說明在整個初始化過程中先後建立了兩個 IServiceProvider
物件。
即 ASP.NET Core 的初始化包含了兩個步驟:
- 第一個步驟是 Hosting 相關服務的初始化過程,初始化完畢之後建立了第一個
IServiceProvider
物件; - 第二步是 Application 相關服務的初始化過程。而 Application 的初始化過程可以註入 Hosting 相關的服務。之後,透過
IStartup.ConfigureServices
方法建立了第二個IServiceProvider
物件。
如果你對原始碼感興趣
請參考
WebHostBuilder
類的Build
方法(原始碼在這裡:https://github.com/aspnet/AspNetCore/blob/master/src/Hosting/Hosting/src/WebHostBuilder.cs)。大致的過程如下:
BuildCommonServices
方法將所有 Hosting 所需的服務(WebHost
相關型別以及所有IWebHostBuilder
呼叫中新增的服務型別)新增到IServiceCollection
物件中。- 使用該
IServiceCollection
建立 Hosting 相關的IServiceProvider
,不妨稱之為hostingServiceProvider
。- 使用該
hostingServiceProvider
建立IStartup
物件(這裡有和環境相關的 Convension,詳情請參見上一篇)。- 使用一個複製的
IServiceCollection
物件呼叫IStartup.ConfigureServices
方法建立另外一個IServiceProvider
不妨稱之為applicationServiceProvider
。
在瞭解了上述過程之後,那麼我們需要註意些什麼呢?
首先我們已經瞭解,Startup
可以使用 Hosting 的 IServiceProvider
進行註入。但是 IServiceProvider
是一個頂級的 Provider,如果我們在 Startup
中建立了一個非常消耗資源的物件(實現了 IDisposable
),則在預設情況下該物件只有在應用程式徹底退出的時候才會銷毀。若顯式 Dispose
該物件的話且該物件不是 Transient
Scope。則有可能導致 Defect。
4 規避初始化過程中的資源洩露
但是如果我真的需要在初始化的時候註入非常消耗資源的物件,而我又希望規避資源的洩露,我該怎麼辦呢?其實還是有辦法的。那就是不使用 Startup
的建構式進行註入而是直接在 Configure
方法中透過引數進行註入。
為什麼這種方式可以規避資源洩露呢?因為這種註入機智並非典型的依賴註入機制,而是 ASP.NET Core 特意實現的。如果應用程式在初始化時使用的 UseStartup()
中的 TStartup
並沒有實現 IStartup
的話,ASP.NET Core 就會使用基於約定的 IStartup
實現對 TStartup
進行包裝。在包裝過程中,它會嘗試找到 TStartup
型別中的 Configure
方法,檢查引數表中的引數,並使用 IStartup.ConfigureServices
建立的 IServiceProvider
進行註入。但是這裡的 IServiceProvider
卻並不初始化過程中的頂級 Provider。而是在將整個方法呼叫包裹在了 Scope
裡。因此即使在初始化過程中建立非常消耗資源的實體也會隨著方法呼叫結束後 Scope
的 Dispose
而銷毀。具體程式碼請參見:ConfigureBuilder
原始碼 (https://github.com/aspnet/AspNetCore/blob/master/src/Hosting/Hosting/src/Internal/ConfigureBuilder.cs)