歡迎光臨
每天分享高質量文章

ASP.NET Core 沉思錄 – ServiceProvider 的二度出生

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<> 以及 ILoggerFactoryILogger<>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 中註入的 configurationlifetimelogger 這些服務是由哪一個 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 裡。因此即使在初始化過程中建立非常消耗資源的實體也會隨著方法呼叫結束後 ScopeDispose 而銷毀。具體程式碼請參見:ConfigureBuilder 原始碼 (https://github.com/aspnet/AspNetCore/blob/master/src/Hosting/Hosting/src/Internal/ConfigureBuilder.cs)

    贊(0)

    分享創造快樂