1. 程式人生 > >ASP.NET Core 執行原理解剖[2]-Hosting補充之配置介紹

ASP.NET Core 執行原理解剖[2]-Hosting補充之配置介紹

在上一章中,我們介紹了 ASP.NET Core 的啟動過程,主要是對 WebHost 原始碼的探索。而本文則是對上文的一個補充,更加偏向於實戰,詳細的介紹一下我們在實際開發中需要對 Hosting 做一些配置時經常用到的幾種方式。

目錄

本系列文章將會從原始碼分析來講解 ASP.NET Core 的執行原理,分為以下幾個章節:

ASP.NET Core 執行原理解剖[1]:Hosting

ASP.NET Core 執行原理解剖[2]:Hosting補充之配置介紹(Current)

  1. WebHostBuild
  2. ISartup
  3. IHostingStartup
  4. IStartupFilter
  5. IHostedService
  6. IApplicationLifetime

ASP.NET Core 執行原理解剖[3]:Middleware-請求管道的構成(待續)

ASP.NET Core 執行原理解剖[4]:進入HttpContext的世界(待續)

ASP.NET Core 執行原理解剖[5]:Authentication(待續)

WebHostBuild

WebHostBuild 用來構建 WebHost ,也是我們最先接觸的一個類,它提供瞭如下方法:

ConfigureAppConfiguration

Configuration 在 ASP.NET Core 進行了全新的設計,使其更加靈活簡潔,可以支援多種資料來源。在 ASP.NET Core 1.x 中,我們是在Startup

的建構函式中配置各種資料來源的,而在 ASP.NET Core 2.0 中則移動了到Program中,這樣能與控制檯應用程式保持一致:

public static class WebHostBuilderExtensions
{
    public static IWebHostBuilder ConfigureAppConfiguration(this IWebHostBuilder hostBuilder, Action<IConfigurationBuilder> configureDelegate)
    {
        return hostBuilder.ConfigureAppConfiguration((context, builder) => configureDelegate(builder));
    }
}

public class WebHostBuilder : IWebHostBuilder
{
    private List<Action<WebHostBuilderContext, IConfigurationBuilder>> _configureAppConfigurationBuilderDelegates;
    public IWebHostBuilder ConfigureAppConfiguration(Action<WebHostBuilderContext, IConfigurationBuilder> configureDelegate)
    {
        if (configureDelegate == null)
        {
            throw new ArgumentNullException(nameof(configureDelegate));
        }

        _configureAppConfigurationBuilderDelegates.Add(configureDelegate);
        return this;
    }
}

_configureAppConfigurationBuilderDelegates委託會在 WebHostBuilder 的Build方法中執行,生成 IConfiguration 物件並以單例的形式註冊到 DI 系統中, 我們可以在Startup以及應用程式的任何地方,通過 DI 系統來獲取到。

而在 上一章 中也介紹過,在CreateDefaultBuilder中會通過該方法來新增appsettinggs.json等基本配置的配置源。

UseSetting

UseSetting 是一個非常重要的方法,它用來配置 WebHost 中的 IConfiguration 物件。需要注意與上面ConfigureAppConfiguration的區別, WebHost 中的 Configuration 只限於在 WebHost 使用,並且我們不能配置它的資料來源,它只會讀取ASPNETCORE_開頭的環境變數:

private IConfiguration _config;

public WebHostBuilder()
{
    _config = new ConfigurationBuilder()
        .AddEnvironmentVariables(prefix: "ASPNETCORE_")
        .Build();
}

而我們比較熟悉的當前執行環境,也是通過該_config來讀取的,雖然我們不能配置它的資料來源,但是它為我們提供了一個UseSetting方法,為我們提供了一個設定_config的機會:

public string GetSetting(string key)
{
    return _config[key];
}

而我們通過UseSetting設定的變數最終也會以MemoryConfigurationProvider的形式新增到上面介紹的ConfigureAppConfiguration所配置的IConfiguration物件中。

UseStartup

UseStartup 這個我們都比較熟悉,它用來顯式註冊我們的Startup類,可以使用泛性,Type , 和程式集名稱三種方式來註冊:


// 常用的方法
public static IWebHostBuilder UseStartup<TStartup>(this IWebHostBuilder hostBuilder) where TStartup : class
{
    return hostBuilder.UseStartup(typeof(TStartup));
}

// 通過指定的程式集來註冊 Startup 類
public static IWebHostBuilder UseStartup(this IWebHostBuilder hostBuilder, string startupAssemblyName)
{
    if (startupAssemblyName == null)
    {
        throw new ArgumentNullException(nameof(startupAssemblyName));
    }

    return hostBuilder
        .UseSetting(WebHostDefaults.ApplicationKey, startupAssemblyName)
        .UseSetting(WebHostDefaults.StartupAssemblyKey, startupAssemblyName);
}

// 最終的 Startup 類註冊方法,上面兩種只是一種簡寫形式
public static IWebHostBuilder UseStartup(this IWebHostBuilder hostBuilder, Type startupType)
{
    ....
}

具體的註冊方式,在 上一章 也介紹過,就是通過反射建立例項,然後注入到 DI 系統中。

ConfigureLogging

ConfigureLogging 用來配置日誌系統,在 ASP.NET Core 1.x 中是在Startup類的Configure方法中,通過ILoggerFactory擴充套件來註冊的,在 ASP.NET Core 中也變得更加簡潔,並且統一通過 WebHostBuild 來配置:

public static class WebHostBuilderExtensions
{
    public static IWebHostBuilder ConfigureLogging(this IWebHostBuilder hostBuilder, Action<ILoggingBuilder> configureLogging)
    {
        return hostBuilder.ConfigureServices(collection => collection.AddLogging(configureLogging));
    }

    public static IWebHostBuilder ConfigureLogging(this IWebHostBuilder hostBuilder, Action<WebHostBuilderContext, ILoggingBuilder> configureLogging)
    {
        return hostBuilder.ConfigureServices((context, collection) => collection.AddLogging(builder => configureLogging(context, builder)));
    }
}

AddLogging 是Microsoft.Extensions.Logging提供的擴充套件方法,更具體的可以看我之前介紹的 ASP.NET Core 原始碼學習之 Logging 系列。

ConfigureServices

在上面的幾個方法中,多次用到 ConfigureServices,而 ConfigureServices 與 Starup 中的 ConfigureServices 類似,都是用來註冊服務的:

private readonly List<Action<WebHostBuilderContext, IServiceCollection>> _configureServicesDelegates;

public IWebHostBuilder ConfigureServices(Action<WebHostBuilderContext, IServiceCollection> configureServices)
{
    if (configureServices == null)
    {
        throw new ArgumentNullException(nameof(configureServices));
    }

    _configureServicesDelegates.Add(configureServices);
    return this;
}

但不同的是_configureServicesDelegates的執行時機較早,是在WebHostBuilder的Build方法中執行的,所以會參與 WebHost 中hostingServiceProvider的構建。

其它

WebHostBuild 中還有很多配置的方法,就不再一一細說,在這裡簡單介紹一下:

  • UseContentRoot 使用UseSetting方法配置IConfiguration["contentRoot"],表示應用程式所在的預設資料夾地址,如 MVC 中檢視的查詢根目錄。

  • UseWebRoot 使用UseSetting方法配置IConfiguration["webroot"],用來指定可讓外部可訪問的靜態資源路徑,預設為wwwroot,並且是以contentRoot為根目錄。

  • CaptureStartupErrors 使用UseSetting方法配置IConfiguration["captureStartupErrors"],表示是否捕捉啟動時的異常,如果為ture,則在啟動時發生異常也會啟動 Http Server,並顯示錯誤頁面,否則,不會啟動 Http Server。

  • UseEnvironment 使用UseSetting方法配置IConfiguration["environment"],用來指定執行環境。

  • UseServer 用來配置 Http Server 服務,UseKestrel便是此方法的簡寫形式。

  • UseUrls 使用UseSetting方法配置IConfiguration["urls"],用來配置 Http 伺服器地址,多個使用;分割。

  • UseShutdownTimeout 使用UseSetting方法配置IConfiguration["shutdownTimeoutSeconds"],用來設定 ASP.NET Core 停止時等待的時間。

  • DetailedErrors 表示是否顯示詳細的錯誤資訊,可為true/false1/0,預設為 false,但它沒有提供直接配置的方法,可以通過UseSetting來指定IConfiguration["detailedErrors"]

ISartup

ISartup 是我們比較熟悉的,因為在我們建立一個預設的 ASP.NET Core 專案時,都會有一個Startup.cs檔案,包含三個約定的方法,按執行順序排列如下:

1. ConfigureServices

ASP.NET Core 框架本身提供了一個 DI(依賴注入)系統,並且可以非常靈活的去擴充套件,很容易的切換成其它的 DI 框架(如 Autofac,Ninject 等)。在 ASP.NET Core 中,所有的例項都是通過這個 DI 系統來獲取的,並要求我們的應用程式也使用 DI 系統,以便我們能夠開發出更具彈性,更易維護,測試的應用程式。總之在 ASP.NET Core 中,一切皆注入。關於 “依賴注入” 這裡就不再多說。

在 DI 系統中,想要獲取服務,首先要進行註冊,而ConfigureServices方法便是用來註冊服務的。

public void ConfigureServices(IServiceCollection services)
{
    services.AddScoped<IUserService, UserService>();
}

如上,我們為IUserService介面註冊了一個UserService型別的例項。

2. ConfigureContainer(不常用)

ConfigureContainer 是用來替換 DI 框架的,如下,我們將 ASP.NET Core 內建的 DI 框架替換為 Autofac

public void ConfigureContainer(ContainerBuilder builder)
{
    builder.RegisterModule(new AutofacModule());
}

雖然 ASP.NET Core 自帶的 DI 系統只提供了建構函式注入,以及不支援命名例項等,但我喜歡它的簡潔,並且不太喜歡依賴太多第三庫,一直也只使用了內建的DI框架,因此對這個方法也不太瞭解,就不再多說。

3. Configure

Configure 接收一個IApplicationBuilder型別引數,而IApplicationBuilder在 上一章 中介紹過,它是用來構建請求管道的,因此,也可以說 Configure 方法是用來配置請求管道的,通常會在這裡會註冊一些中介軟體。

public void Configure(IApplicationBuilder app)
{
    app.Use(next =>
    {
        return async (context) =>
        {
            await context.Response.WriteAsync("Hello ASP.NET Core!");
        };
    });
}

所謂中介軟體,也就是對 HttpContext 進行處理的一種便捷方式,下文會詳細來介紹。而如上程式碼,我們註冊了一個最簡單的中介軟體,通過瀏覽器訪問,便可以看到 “Hello ASP.NET Core!” 。

通常,我們的 Startup 類並沒有去實現IStartup介面,這是因為我們在Configure方法中,大多時候可能需要獲取一些其它的服務,如我剛才註冊的IUserService,我們可以直接新增到 Configure 方法的引數列表當中:

public void Configure(IApplicationBuilder app, IUserService userService) { }

ASP.NET Core 會通過 DI 系統來解析到 userService 例項,但是 ASP.NET Core 中的 DI 系統是不支援普通方法的引數注入的,而是手動通過反射的方式來實現的:

services.AddSingleton(typeof(IStartup), sp =>
{
    var hostingEnvironment = sp.GetRequiredService<IHostingEnvironment>();
    var methods = StartupLoader.LoadMethods(sp, startupType, hostingEnvironment.EnvironmentName);
    return new ConventionBasedStartup(methods);
});

而通過反射也可以為我們帶來更大的靈活性,上面的LoadMethods方法會根據當前的執行環境名稱來查詢適當的方法名:

public static StartupMethods LoadMethods(IServiceProvider hostingServiceProvider, Type startupType, string environmentName)
{
    var configureMethod = FindConfigureDelegate(startupType, environmentName);
}

private static ConfigureBuilder FindConfigureDelegate(Type startupType, string environmentName)
{
    var configureMethod = FindMethod(startupType, "Configure{0}", environmentName, typeof(void), required: true);
    return new ConfigureBuilder(configureMethod);
}

更具體的可以檢視 StartupLoader,ASP.NET Core 會根據當前環境的不同,而執行不同的方法:

public void ConfigureServices(IServiceCollection services) { }

public void ConfigureDevelopmentServices(IServiceCollection services) { }

public void ConfigureContainer(ContainerBuilder builder) {}

public void ConfigureDevelopmentContainer(ContainerBuilder builder) { }

public void Configure(IApplicationBuilder app) { }

public void ConfigureDevelopment(IApplicationBuilder app) { }

如上,當在Development環境上執行時,會選擇帶Development的方法來執行。

而在預設模版中是通過UseStartup<Startup>的方式來註冊 Startup 類的,我們也可以使用上面介紹的指定程式集名稱的方式來註冊:

public static IWebHost BuildWebHost(string[] args) =>
    WebHost.CreateDefaultBuilder(args)
        .UseStartup("EmptyWebDemo")
        .Build();

如上,我們指定在 EmptyWebDemo 中查詢Startup類,這樣還有一個額外的好處,WebHost 同樣會根據當前的執行環境來選擇不同的Startup類(如StartupDevelopment),與上面介紹的Startup中方法的查詢方式一樣。

IHostingStartup

上面,我們介紹了Sartup,而一個專案中只能一個Sartup,因為如果配置多個,則最後一個會覆蓋之前的。而在一個多層專案中,Sartup類一般是放在展現層中,我們在其它層也需要註冊一些服務或者配置請求管道時,通常會寫一個擴充套件方法:

public static class EfRepositoryExtensions
{
    public static void AddEF(this IServiceCollection services,string connectionStringName)
    {    
        services.AddDbContext<AppDbContext>(options =>
            options.UseSqlServer(connectionStringName), opt => opt.EnableRetryOnFailure())
        );

        services.TryAddScoped<IDbContext, AppDbContext>();
        services.TryAddScoped(typeof(IRepository<,>), typeof(EfRepository<,>));

        ...
    }

    public static void UseEF(IApplicationBuilder app)
    {
        app.UseIdentity();
    }
}

然後在 Startup 中呼叫這些擴充套件方法:

public void ConfigureDevelopmentServices(IServiceCollection services)
{
    services.AddEF(Configuration.GetConnectionString("DefaultConnection");
}

public void ConfigureDevelopment(IApplicationBuilder app)
{
    services.UseEF();
}

感覺這種方式非常醜陋,而在上一章中,我們知道 WebHost 會在 Starup 這前呼叫 IHostingStartup,於是我們便以如下方式來實現:

[assembly: HostingStartup(typeof(Zero.EntityFramework.EFRepositoryStartup))]
namespace Zero.EntityFramework
{
    public class EFRepositoryStartup : IHostingStartup
    {
        public void Configure(IWebHostBuilder builder)
        {
            builder.ConfigureServices(services =>
            {
                services.AddDbContext<AppDbContext>(options =>
                    options.UseSqlServer(connectionStringName), opt => opt.EnableRetryOnFailure())
                );

                services.TryAddScoped<IDbContext, AppDbContext>();
                services.TryAddScoped(typeof(IRepository<,>), typeof(EfRepository<,>));

                ...
            }); 

            builder.Configure(app => {
                app.UseIdentity();
            });
        }
    }
}

如上,只需實現 IHostingStartup 介面,要清爽簡單的多,怎一個爽字了得!不過,還需要進行註冊才會被WebHost執行,首先要指定HostingStartupAttribute程式集特性,其次需要配置 WebHost 中的 IConfiguration[hostingStartupAssemblies],以便 WebHost 能找到我們的程式集,可以使用如下方式配置:

WebHost.CreateDefaultBuilder(args)
    // 如需指定多個程式集時,使用 ; 分割
    .UseSetting(WebHostDefaults.HostingStartupAssembliesKey, "Zero.Application;Zero.EntityFramework")

這樣便完成了 IHostingStartup 註冊,不過還需要將包含IHostingStartup的程式集放到 Bin 目錄下,否則根本無法載入。不過 ASP.NET Core 也提供了類似外掛的方式來指定IHostingStartup程式集的查詢位置,可通過設定DOTNET_ADDITIONAL_DEPSASPNETCORE_HOSTINGSTARTUPASSEMBLIES來實現,而這裡就不再多說。

IHostingStartup 是由 WebHostBuilder 來呼叫的,執行時機較早,在建立 WebHost 之前執行,因此可以替換一些在 WebHost 中需要使用的服務。

IStartupFilter

IStartupFilter 是除StartupHostingStartup之處另一種配置IApplicationBuilder的方式:

public interface IStartupFilter
{
    Action<IApplicationBuilder> Configure(Action<IApplicationBuilder> next);
}

它只有一個Configure方法,是對 Starup 類中Configure方法的攔截器,給我們一個在Configure方法執行之前進行一些配置的機會。

讓我們實踐一把,先定義2個 StartupFilter:

public class A : IStartupFilter
{
    public Action<IApplicationBuilder> Configure(Action<IApplicationBuilder> next)
    {
        Console.WriteLine("This is A1!");
        return app =>
        {
            Console.WriteLine("This is A2!");
            next(app);
        };
    }
}

public class B : IStartupFilter
{
    public Action<IApplicationBuilder> Configure(Action<IApplicationBuilder> next)
    {
        Console.WriteLine("This is B1!");
        return app =>
        {
            Console.WriteLine("This is B2!");
            next(app);
        };
    }
}

然後讓他們註冊到DI系統中,WebHost 在執行 Starup 類中Configure方法之前,會從 DI 系統中獲取所有的IStartupFilter來執行:

public void ConfigureServices(IServiceCollection services)
{
    services.AddSingleton<IStartupFilter, A>();
    services.AddSingleton<IStartupFilter, B>();
}

public void Configure(IApplicationBuilder app)
{
    Console.WriteLine("This is Configure!");
    app.Use(next =>
    {
        return async (context) =>
        {
            await context.Response.WriteAsync("Hello ASP.NET Core!");
        };
    });
}

最終,它他的執行順序為:B1 -> A1 -> A2 -> B2 -> Configure 。

IHostedService

當我們希望隨著 ASP.NET Core 的啟動,來執行一些後臺任務(如:定期的重新整理快取等)時,並在 ASP.NET Core 停止時,可以優雅的關閉,則可以使用IHostedService,它有如下定義:

public interface IHostedService
{
    Task StartAsync(CancellationToken cancellationToken);

    Task StopAsync(CancellationToken cancellationToken);
}

很簡單,只有開始和停止兩個方法,它的用法大概是這個樣子的:

public class CacheHostService : IHostedService
{
    private readonly ICacheService _cacheService;
    private CancellationTokenSource _cts;
    private Task _executingTask;

    public CacheHostService(ICacheService cacheService)
    {
        _cacheService = cacheService;
    }

    public Task StartAsync(CancellationToken cancellationToken)
    {
        _cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
        _executingTask = Task.Run(async () =>
            {
                while (!_cts.IsCancellationRequested)
                {
                    Console.WriteLine("cancellationToken:" + _cts.IsCancellationRequested);
                    await _cacheService.Refresh();
                    await Task.Delay(TimeSpan.FromSeconds(5), cancellationToken);
                }
            });
        return Task.CompletedTask;
    }

    public async Task StopAsync(CancellationToken cancellationToken)
    {
        // 傳送停止訊號,以通知我們的後臺服務結束執行。
        _cts.Cancel();

        // 等待後臺服務的停止,而 ASP.NET Core 大約會等待5秒鐘(可在上面介紹的UseShutdownTimeout方法中配置),如果還沒有執行完會發送取消訊號,以防止無限的等待下去。
        await Task.WhenAny(_executingTask, Task.Delay(-1, cancellationToken));

        cancellationToken.ThrowIfCancellationRequested();
    }
}

如上,我們定義了一個在臺後每5秒重新整理一次快取的服務,並在 ASP.NET Core 程式停止時,優雅的關閉。最後,將它註冊到 DI 系統中即可:

public void ConfigureServices(IServiceCollection services)
{
    services.AddSingleton<ICacheService, CacheService>();
    services.AddSingleton<IHostedService, CacheHostService>();
}

WebHost 在啟動 HTTP Server 之後,會從 DI 系統中獲取所有的IHostedService,來啟動我們註冊的 HostedService,參見上一章 。

IApplicationLifetime

IApplicationLifetime用來實現 ASP.NET Core 的生命週期鉤子,我們可以在 ASP.NET Core 停止時做一些優雅的操作,如資源的清理等。它有如下定義:

public interface IApplicationLifetime
{
    CancellationToken ApplicationStarted { get; }

    CancellationToken ApplicationStopping { get; }

    CancellationToken ApplicationStopped { get; }

    void StopApplication();
}

IApplicationLifetime已被 ASP.NET Core 註冊到 DI 系統中,我們使用的時候,只需要注入即可。它有三個CancellationToken型別的屬性,是非同步方法終止執行的訊號,表示 ASP.NET Core 生命週期的三個階段:啟動,開始停止,已停止。

public void Configure(IApplicationBuilder app, IApplicationLifetime appLifetime)
{
    appLifetime.ApplicationStarted.Register(() => Console.WriteLine("Started"));
    appLifetime.ApplicationStopping.Register(() => Console.WriteLine("Stopping"));
    appLifetime.ApplicationStopped.Register(() =>
    {
        Console.WriteLine("Stopped");
        Console.ReadKey();
    });

    app.Use(next =>
    {
        return async (context) =>
        {
            await context.Response.WriteAsync("Hello ASP.NET Core!");
            appLifetime.StopApplication();
        };
    });
}

執行結果如下:

appLifetime_demo

在上一章中我們提到過, IApplicationLifetime 的啟動訊號是在 WebHostStartAsync方法中觸發的,而沒有提到停止訊號的觸發,在這裡補充一下:

internal class WebHost : IWebHost
{
    public async Task StopAsync(CancellationToken cancellationToken = default(CancellationToken))
    {
        ....

        // 設定 Task 的超時時間,上文在 IHostedService 中提到過
        var timeoutToken = new CancellationTokenSource(Options.ShutdownTimeout).Token;
        if (!cancellationToken.CanBeCanceled)
        {
            cancellationToken = timeoutToken;
        }
        else
        {
            cancellationToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutToken).Token;
        }

        // 觸發 Stopping 訊號
        _applicationLifetime?.StopApplication();

        // 停止 Http Server
        if (Server != null)
        {
            await Server.StopAsync(cancellationToken).ConfigureAwait(false);
        }

        // 停止 我們註冊的 IHostService
        if (_hostedServiceExecutor != null)
        {
            await _hostedServiceExecutor.StopAsync(cancellationToken).ConfigureAwait(false);
        }

        // 傳送 Stopped 通知
        _applicationLifetime?.NotifyStopped();
    }
}

總結

本文詳細介紹了對 WebHost 的配置,結合 上一章,對 ASP.NET Core 的啟動流程也基本清楚了,下一章就來介紹一下請求管道的建立,敬請期待!

參考資料:

  • ASP-NET-Core-2-IHostedService

  • ASPNET-Core-2.0-Stripping-Away-Cross-Cutting-Concerns

  • Looking-at-asp-net-cores-iapplicationlifetime