ASP.NET Core 沉思录 - 环境的思考
我的博客換新家啦,新的地址為:https://clrdaily.com :-D
今天我們來一起思考一下如何在不同的環境應用不同的配置。這里的配置不僅僅指 IConfiguration 還包含 IWebHostBuilder 的創建過程和 Startup 的初始化過程。
0 太長不讀
環境造成的差異在架構中基本體現在 Infrastructure 中的各個 Adapter 中。而不應當入侵應用程序內部
在 ASP.NET Core 中我們需要考慮如何將這些 Adapter(一)放在 service collection 中 (二)(可選)添加到 pipeline 中。
ASP.NET Core 默認提供了一系列手段來判斷當前的環境,只不過這些手段的設計奇怪且不完整。
IWebHostBuilder 的配制方法大多和環境相關,但 UseSetting 和環境無關。
我們應當應用開閉原則,將相同環境的配置聚合起來,不同環境的配置進行統一抽象。方便維護和擴展。
當我們進行設計的時候,需要注意不要將思路局限在 Framework 的設計上,而應當切實考慮我們真正希望解決的問題。
1 架構層面的思考
Web Service 的開發和部署過程會涉及若干環境。總的來說可以分為開發環境和部署環境。而部署環境往往又分為 QA、Stage 和 Production 等。對于不同的環境,應用程序可能需要應用不同的配置或實現。還是回到架構的層面上,如下圖:
那么這種不同應該體現在架構的哪一個層面上呢?應當讓這些不同體現在 Infrastructure 的那些 Adapters 上。因為 Adapter 是其中直接和環境相關的部分。
用一個典型的例子來表示。假定一個注冊用戶 Account 的業務。在 Application Service 層面,我們提供了如下的接口:
public?class?AccountRegistrationService?{????public?AccountRegistrationResult?Register(AccountRegistrationRequest?request)?{
????????Account?account?=?this.repository.CreateDetached();
????????//?initialize?account?from?request
????????account.Save();
????????return?AccountRegistrationResult.Create(account);
????}
}
在 Domain 層面我們有代表領域對象 Account 的類型 Account。Account 類型的 Save() 方法可以保存賬戶信息,其中的實現類似:
public?class?Account?{????...
????public?void?Save()?{
????????this.repository.Save(this);
????}
}
而其中的 repository 則依賴 UnitOfWork 而 UnitOfWork 則可能依賴于具體的持久化實現或者依賴于其他遠程服務:
public?class?AccountRepository?{????readonly?IUnitOfWork?session;
????public?Account?CreateDetached()?{
????????return?new?Account(this);
????}
????public?void?Save(Account?account)?{
????????this.session.RegisterNew(account);
????}
}
在這個例子中,AccountService 屬于 Application Service 層面,Account 和 AccountRepository 則屬于 Domain 層面。這兩層的依賴關系是 Application Service 依賴于 Domain。而 Domain 中的 UnitOfWork 則是一個接口。假設我們需要將數據寫入數據庫。則這個接口的實現需要持久化的支持例如它需要使用特定的 IDbConnection (Adapter)。即 IUnitOfWork 的實現位于 Infrastructure 層,并在 Infrastructure 層調用 Adapter 向 DB 中寫入信息。
而對于不同的環境則可以使用不同的實現,例如,對于運行單元測試的環境,我們不妨叫她 Test 環境。這個 DB 很有可能是一個 in memory 的 SQLite 數據庫。而在生產環境則是 MySQL 的集群。
應用程序的內部邏輯最終全部依賴與特定的抽象或接口。它們全部嚴密的包裹在 Infrastructure 之中,并和外部環境完全隔離。而 Infrastructure 中的 Adapter 則負責聯系外部環境。綜上所述,環境相關的變化應當全部封閉在 Infrastructure 中。
2 ASP.NET Core 中的對應關系
ASP.NET Core 應用程序中的組件的初始化由兩個部分構成,第一個部分就是將組件中的類型添加到依賴注入的 IServiceCollection 實例中,以便進行創建;第二個部分(可選)即將組件通過 IApplicationBuilder 添加到應用程序的處理流水線中。我們一個一個來思考。
2.1 依賴注入
ASP.NET Core Web Application 中用依賴注入來決定某種抽象的實現類型。但需要指出的是 ASP.NET 應用程序的依賴注入是分兩個階段進行的。(我們將在另外一篇中介紹),簡單來說 ServiceCollection 的構建分為兩個部分:
為了構建宿主環境而添加的類型;(Infrastructure 層)
為了應用程序本身而添加的 Framework(例如 MvC)和各種業務類型。(Infrastructure 層,Application + Domain 層)。
而和環境相關的部分主要位于 “為了構建宿主環境而添加的類型” 中。這一部分的代碼屬于在 IStartup 初始化之前的 WebHostBuilder 構建代碼中。一般來說,我們習慣于將 UseStartup 調用放在 IWebHostBuilder 實例創建的最后,那么也就是 UseStartup 之前的代碼:
public?static?IWebHostBuilder?CreateWebHostBuilder(string[]?args){
????return?new?WebHostBuilder()
????????.UseKestrel()
????????.ConfigureLogging(...)
????????//
????????//?The?configurations?before?UseStartup?are?environment?specific
????????//
????????.UseStartup<Startup>();
}
2.2 流水線
在流水線配置中主要考慮的是 Web 輸入輸出上的的變化。例如 Production 環境需要配置 SSL,消除敏感 Header,消除詳細的 Error Information 等等。
將組件配置到應用程序的流水線的操作是在 IStartup 接口的實現中進行的。定義 IStartup 接口實現的方式大體有兩種,第一種是調用 WebHostBuilderExtensions.Configure 方法,另一種是使用 WebHostBuilderExtensions.UseStartup 方法。不論使用何種方式最終都會歸結到對 IApplicationBuilder 的操作:
public?void?Configure(IApplicationBuilder?app)?{????//?building?pipeline
}
在這個時候,宿主初始化相關的類型已經全部可以使用了。因此取用環境相關的信息(環境類型,配置等)就更方便了。
3 落地
ASP.NET Core 對這個環節的設計很奇怪。一方面,它提供了非常底層的基于 IHostingEnvironment.EnvironmentName 的值來進行環境區分的方法。例如,官方范例中往往會使用如下的代碼:
new?WebHostBuilder()????.UseKestrel()
????.ConfigureLogging((context,?logBuilder)?=>?{
????????if?(context.HostingEnvironment.IsDevelopment())?{
????????????...
????????}
????????else?if?(context.HostingEnvironment.IsProduction())?{
????????????...
????????}
????????else?{
????????????...
????????}
????})
????...
而另一方面卻又在 Startup 上設計了命名的 Convension。例如:
class?DevelopmentStartup?{}?????//?for?Developmentclass?ProductionStartup?{}??????//?for?Production
class?Startup?{}????????????????//?fallback
...
webHostBuilder.UseStartup(assemblyName);
又例如:
class?Startup??{????public?void?ConfigureServices(IServiceCollection?services)?{?}
????public?void?ConfigureStagingServices(IServiceCollection?services)?{?}
????public?void?Configure(IApplicationBuilder?app,?IHostingEnvironment?env)?{?}
????public?void?ConfigureStaging(IApplicationBuilder?app,?IHostingEnvironment?env)?{?}
}
這些設計差異很大且每一個都不徹底。而在實際項目中環境屬于一個擴展點;而每一套環境的各項配置應當是內聚的。因此上述幾種方式或多或少會增加維護上的成本。而較好的設計應當針對如下三個問題:
能夠立刻說出,我的系統支持幾種環境;
每一種環境的各種類型的配置(例如,配置源、日志記錄、HTTP Client、數據庫)是什么樣子的,有什么差異;
能不能用兩步添加一個新的環境:第一,一次性創建一個新環境的所有配置,第二,將這個環境納入到系統初始化過程中。
為了達到這個要求,需要考慮統一的實現手段。
3.1 在 WebHost 開始構建之前我們并不能確定環境信息
一個最簡單的想法就是根據不同的環境采取兩種完全不同的 WebHostBuilder 配置流程。例如:
WebHostBuilder?builder?=?new?WebHostBuilderFactory().Create(env.EnvironmentName);遺憾的是這種設計本身是有問題的。首先,若干環節都可以影響環境的最終確定,包括:
當前 Session 的 ASPNETCORE_ENVIRONMENT 的值;(請參見 https://github.com/aspnet/AspNetCore/blob/master/src/Hosting/Hosting/src/WebHostBuilder.cs#L44)
Properties/launchSettings.json 中選定 Profile 中 ASPNETCORE_ENVIRONMENT 的值(如果用 dotnet run 命令執行的話)
WebHostBuilder.UseEnvironment(name) 的參數值;
WebHostBuilder.UseSetting(key, value) 當 key 為 WebHostDefaults.EnvironmentKey 時的值。
若 Host 在 IIS 中,則 web.config 中關于 environmentVariable 的設置。
因此只有在 WebHostBuilder 開始 Build 時,我們才可以最終確定環境名稱。
3.2 `UseSetting` 并不是環境相關的
另一種方案是包裝 IWebHostBuilder 使其能夠依據環境做出相應的 Dispatch。例如:
abstract?class?EnvironmentAwareWebHostBuilder?:?IWebHostBuilder?{????IWebHostBuilder?UnderlyingBuilder?{?get;?}
????protected?abstract?bool?IsSupported(IHostingEnvironment?hostingEnvironment);
????protected?EnvironmentAwareWebHostBuilder(IWebHostBuilder?underlyingBuilder)
????{
????????//?Validation?omitted
????????UnderlyingBuilder?=?underlyingBuilder;
????}
????//?...
}
從而我們可以分別為不同的環境進行相應的配置。以 ConfigureService 方法為例:
public?IWebHostBuilder?ConfigureServices(Action<IServiceCollection>?configureServices){
????UnderlyingBuilder.ConfigureServices(
????????(context,?services)?=>
????????{
????????????if?(!IsSupported(context.HostingEnvironment))?{?return;?}
????????????configureServices(services);
????????});
????return?this;
}
按照上述方式包裝 ConfigureAppConfiguration,這樣就可以構造以下的擴展方法:
public?static?IWebHostBuilder?UseEnvironment(????this?IWebHostBuilder?builder,
????string?environmentName,?
????Action<IWebHostBuilder>?configureBuilder)
{
????bool?IsEnvironmentSupported(IHostingEnvironment?h)?=>?
????????h.IsEnvironment(config.environmentName);
????EnvironmentAwareWebHostBuilder?environmentAwareBuilder?=
????????new?DelegatedWebHostBuilder(builder,?IsEnvironmentSupported);
????config.configureBuilder(environmentAwareBuilder);
????return?builder;
}
這種方案下的 WebHostBuilder 初始化邏輯就變成了:
webHostBuilder????.UseEnvironment("Development",?wb?=>?{
????????wb
????????????.ConfigureService((ctx,?cb)?=>?{?...?})
????????????.ConfigureLogging((lb)?=>?{?...?})
????????????...
????})
????.UseEnvironment("Production",?wb?=>?{
????????//?configure?for?production
????});
這樣我們至少就可以用若干擴展方法類將不同環境完全分開了。但是這個實現方案是有問題的:UseSetting 方法。IWebHostBuilder 所公開的方法中除了 Build、ConfigureServices 和 ConfigureAppConfiguration 之外還有第四個方法:UseSetting。和上述 ConfigureXxx 方法不同,UseSetting 方法執行完畢之后其影響馬上生效,而且該方法無法根據不同的環境作出變化。即,如果我們使用了:
webHostBuilder????.UseEnvironment("Development",?wb?=>?wb.UseSetting("Foo",?"Bar"))
????.UseEnvironment("Production",?wb?=>?wb.UseSetting("Foo",?"O_o"));
且當前環境為 Development 則 IConfiguration 實例的 "Foo" 對應的值為 "O_o"。這就會造成混淆。
3.3 還是從擴展點來思考
從第 2 節的論述中我們已經知道和環境相關的配置可能存在于宿主環境初始化過程中,也可能存在 Startup 初始化過程中(即 WebHost.Run 方法執行過程中)。因此我們必須綜合考慮這兩個部分,但是這個兩個部分天生是不同的。那么強行進行統一也是不合適的。
根據開閉原則,我們還是應該從擴展點上來考慮。首先我們能夠確定我們的 Adapter 有哪些。又有哪一些 Adapter 是和環境相關的。例如我們和環境相關的 Adapter 有 DB,配置文件加載,日志記錄,HttpClient(在非 Development 環境中我們可能需要進行客戶端證書驗證),在流水線創建過程中需要根據環境配置是否需要 HTTPS 強制跳轉,需要配置錯誤信息的詳細程度等等。在梳理好這些內容后我們就能有針對性的創建方法對各個部分進行配置了,我們可以使用工廠模式:
class?WebHostConfigureFactory?{????...
????public?IWebHostConfigurator?Create(string?environmentName)?{
????????return?cachedConfigurators[environmentName];
????}
}
而每一個 IWebHostConfigurator 中都包含了所有的環境相關配置:
interface?IWebHostConfigurator?{????void?AddDatabase(IHostingEnvironment?environment,?IServiceCollection?services);
????void?LoadConfiguration(IHostingEnvironment?environment,?IConfigurationBuilder?configBuilder);
????void?ConfigureLogging(IHostingEnvironment?environment,?ILoggingBuilder?loggingBuilder);
????void?AddHttpClient(IHostingEnvironment?environment,?IServiceCollection?services);
????void?ConfigureHttpsRedirection(IHostingEnvironment?environment,?IConfiguration?configuration,?IApplicationBuilder?builder);
????void?ConfigureErrorHandler(IHostingEnvironment?environment,??IConfiguration?configuration,?IApplicationBuilder?builder);
}
而這樣我們為各個環境的擴展點建立了抽象,從而統一配置過程:
static?IWebHostBuilder?CreateWebHostBuilder()?{????return?new?WebHostBuilder()
????????.UseKestrel()
????????//
????????//?Common?configurations
????????//
????????.ConfigureServices((context,?services)?=>?{
????????????IWebHostConfigurator?configurator?=?factory.Create(context.HostingEnvironment.EnvironmentName);
????????????configurator.AddDatabase(context.HostingEnvironment,?services);
????????????configurator.AddHttpClient(context.HostingEnvironment,?services);
????????})
????????.ConfigureLogging((context,?logBuilder)?=>?{
????????????factory
????????????????.Create(context.HostingEnvironment.EnvironmentName)
????????????????.ConfigureLogging(context.HostingEnvironment,?logBuilder);
????????})
????????.UseStartup<Startup>();
}
...
class?Startup?{
????...
????public?void?Configure(IApplicationBuilder?app)?{
????????IWebHostConfigurator?configurator?=?factory.Create(hostingEnvironment.EnvironmentName);
????????configurator.ConfigureHttpsRedirection(hostingEnvironment,?configuration,?app);
????????configurator.ConfigureErrorHandler(hostingEnvironment,?configuration,?app);
????????//?Common?configurations
????}
}
4 總結
請跳到文章開頭 :-D
參考資料
R. C. Martin and R. C. Martin, Clean architecture: a craftsman’s guide to software structure and design. London, England: Prentice Hall, 2018.
Unit-of-work: https://martinfowler.com/eaaCatalog/unitOfWork.html
Dependency Injection in ASP.NET Core: https://docs.microsoft.com/en-us/aspnet/core/fundamentals/dependency-injection?view=aspnetcore-2.2
App startup in ASP.NET Core: https://docs.microsoft.com/en-us/aspnet/core/fundamentals/startup?view=aspnetcore-2.2
Use multiple environments in ASP.NET Core: https://docs.microsoft.com/en-us/aspnet/core/fundamentals/environments?view=aspnetcore-2.2
如果您覺得本文對您有幫助,也歡迎分享給其他的人。我們一起進步。歡迎關注我的微信公眾號:
總結
以上是生活随笔為你收集整理的ASP.NET Core 沉思录 - 环境的思考的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: .Netcore 2.0 Ocelot
- 下一篇: NET Core微服务之路:实战SkyW