在上一章中,我們介紹了 ASP.NET Core 的啟動過程,主要是對 WebHost 源碼的探索。而本文則是對上文的一個補充,更加偏向於實戰,詳細的介紹一下我們在實際開發中需要對 Hosting 做一些配置時經常用到的幾種方式。
目錄
本系列文章從源碼分析的角度來探索 ASP.NET Core 的運行原理,分為以下幾個章節:
ASP.NET Core 運行原理解剖[1]:Hosting
ASP.NET Core 運行原理解剖[2]:Hosting補充之配置介紹(Current)
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/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 的啟動流程也基本清楚了,下一章就來介紹一下請求管道的創建,敬請期待!
參考資料: