ASP.NET Core 執行原理解剖[2]-Hosting補充之配置介紹
在上一章中,我們介紹了 ASP.NET Core 的啟動過程,主要是對 WebHost 原始碼的探索。而本文則是對上文的一個補充,更加偏向於實戰,詳細的介紹一下我們在實際開發中需要對 Hosting 做一些配置時經常用到的幾種方式。
目錄
本系列文章將會從原始碼分析來講解 ASP.NET Core 的執行原理,分為以下幾個章節:
ASP.NET Core 執行原理解剖[1]:Hosting
ASP.NET Core 執行原理解剖[2]:Hosting補充之配置介紹(Current)
- WebHostBuild
- ISartup
- IHostingStartup
- IStartupFilter
- IHostedService
- 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
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/false
或1/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_DEPS
和ASPNETCORE_HOSTINGSTARTUPASSEMBLIES
來實現,而這裡就不再多說。
IHostingStartup 是由 WebHostBuilder 來呼叫的,執行時機較早,在建立 WebHost 之前執行,因此可以替換一些在 WebHost 中需要使用的服務。
IStartupFilter
IStartupFilter 是除Startup
和HostingStartup
之處另一種配置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();
};
});
}
執行結果如下:
在上一章中我們提到過, IApplicationLifetime
的啟動訊號是在 WebHost 的StartAsync
方法中觸發的,而沒有提到停止訊號的觸發,在這裡補充一下:
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