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 的BuildCommonServices方法中執行,最后生成 IConfiguration 對象並以單例的形式注冊到 DI 系統中, 我們可以在Startup以及應用程序的任何地方,通過 DI 系統來獲取到:

foreach (var configureAppConfiguration in _configureAppConfigurationBuilderDelegates)
{
    configureAppConfiguration(_context, builder);
}
var configuration = builder.Build();
services.AddSingleton<IConfiguration>(configuration);

而在 上一章 中也介紹過,在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 的啟動流程也基本清楚了,下一章就來介紹一下請求管道的創建,敬請期待!

參考資料:


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM