ASP.NET Core 菜鳥之路:從Startup.cs說起

風靈使發表於2018-05-23

##1.前言

本文主要是以Visual Studio 2017 預設的 WebApi 模板作為基架,基於Asp .Net Core 1.0,本文面向的是初學者,如果你有 ASP.NET Core 相關實踐經驗,歡迎在評論區補充。
與早期版本的 ASP.NET 對比,最顯著的變化之一就是配置應用程式的方式, Global.asaxFilterConfig.csRouteConfig.cs 統統消失了,取而代之的是 Program.csStartup.csProgram.cs 作為 Web 應用程式的預設入口,不做任何修改的情況下,會呼叫同目錄下 Startup.cs 中的 ConfigureServices 方法 和 Configure 方法。

這裡寫圖片描述
應用啟動的流程

對於初學者來說,第一次面對 Startup.cs 往往無從下手,本文將一步步介紹作者的經驗,但是不會涉入到內部的程式碼實現以及相關的原理,那並不是本文想要討論的範疇。
這裡寫圖片描述
預設的Startup.cs

相信我,這將是你邁出構建靈活而強大的ASP.NET Core 應用程式的第一步。
##2.配置引數選項
在官方文件中提供多種方式來配置引數選項:

檔案格式(INI,JSON和XML)
命令列引數
環境變數
記憶體中的 .NET 物件
使用者機密儲存
Azure 鍵值
自定義提供程式

雖然提供了很多選擇,但是我們只選擇其中的JSON檔案和環境變數來提供配置引數。
###2.1 Json配置引數選項

參考官方文件的示例,我們在 appsettings.json 加入如下的引數:
這裡寫圖片描述
appsettings.json

與此同時,我們還需要一個類來對映這些配置引數:
這裡寫圖片描述
MyOptions.cs

思考一下 subsection 應該是字典還是一個物件?如果是字典,是否可以為<string,dynamic>或者<string,object>

好了,現在就差怎麼讓他們聯絡起來,只需在 ConfigureServices 方法中將他們配對:

  services.Configure<MyOptions>(Configuration);

最後就是解決怎麼使用這些配置引數的問題了,舉個最簡單的例子,我們可以在某個控制器中把我們的所有引數列印出來:
這裡寫圖片描述
MyOptionsController
這裡寫圖片描述
返回結果

不知道你有沒有發現 MyOptions 類中有些屬性首字母大寫,有些屬性沒有,並不是與json檔案中完全一致,也就是說可以忽略大小寫的。

回到之前的匹配環節,我們可以發現 services.Configure 的方法中似乎還有更多選擇,比如我們把之前的那一行程式碼替換為:

services.Configure<MyOptions>(Configuration.GetSection("wizards"));//選擇wizards節點

這裡寫圖片描述
返回結果空了

我們可以發現返回的物件裡面的屬性都為null,這是因為json中的 "wizards"的節點並不能與我們的類匹配。
那麼問題來了,如果匹配的程式碼如下,又會產生什麼樣的結果呢?先別急著回答,我會在下一節中給出答案。
這裡寫圖片描述

###2.2環境變數

環境變數,或者說系統變數,在windows中我們可以在系統屬性中配置:

這裡寫圖片描述
windows中的環境變數

在Linux環境中也有相應的配置,但是在開發過程中,我們可以在 Visual Studio 中配置:

這裡寫圖片描述
vs中配置環境變數

在這之前,我們的Json檔案中已經有 “option1” 和 "option2"的引數選項,那麼會產生什麼樣的結果呢?
這裡寫圖片描述

顯然我們可以看到環境變數的引數覆蓋了Json檔案的引數。而引起這種變化的原因還是需要回到Startup的初始化:

public Startup(IHostingEnvironment env)
{
            var builder = new ConfigurationBuilder()
                .SetBasePath(env.ContentRootPath)
                .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)//必須的json檔案,並且自動過載
                .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true)//不必須的json檔案
                .AddEnvironmentVariables();//啟用環境變數
            Configuration = builder.Build();
}

從中我們可以看出環境變數的配置在讀取 Json 檔案引數之後,這樣就會覆蓋已經存在的同名引數,而已經從 Json 檔案被匹配的引數並不會被清空(同樣適用於前一節提出的問題)。從另一方面來說,如果你不需要環境變數,則需要去掉 "AddEnvironmentVariables() ",以免覆蓋預期引數。
回到本節的中心,我們為什麼會需要環境變數呢?我個人會在Dockerfile中配置一些環境變數,比如某種服務的訪問地址、某中功能的開關等等。下面舉例說說兩個常用的環境變數:
ASPNETCORE_URLS 如果你沒有指定對應的 Url 監聽地址,可以通過該引數修改,如設定為 “http://*:80”。
ASPNETCORE_ENVIRONMENT 開發環境(Development)、預演環境(Staging)、生產環境(Production),將在工作環境一節中做詳細介紹。不同的工作環境將使得整個軟體流程變得清晰。

###2.3配置引數小貼士

  • 引數有多種來源,如不需要勿增來源。
  • 要注意"最近原則",避免引數同名引起衝突。
  • 引數的key可以忽略大小寫,所以環境變數中的 “OPTION2” 可以引起覆蓋 Json檔案中的 “option2” 的效果。
  • 為複雜引數選擇合適的型別很重要,比如字典還是物件的取捨。

##3.依賴注入

依賴注入在 ASP.NET Core 中無處不存在,在之前列印引數的例子中同樣用到。依賴注入好處都有啥?為什麼我們需要依賴注入?在 ASP .NET Core 中文文件中依賴注入的章節 很好地解釋了:

你應該設計你的依賴注入服務來獲取它們的合作者。這意味著在你的服務中避免使用有狀態的靜態方法呼叫(程式碼被稱為 static cling)和直接例項化依賴的型別。當選擇例項化一個型別還是通過依賴注入請求它時,它可以幫助記住這句話, New is Glue。通過遵循物件導向設計的 SOLID 原則,你的類將傾向於小、易於分解及易於測試。

###3.1註冊服務以及簡單使用

為了方便下一節測試,我準備三個檔案,簡單的介面、該介面的實現類,擁有介面成員的類:

這裡寫圖片描述
IRepository

這裡寫圖片描述
MemoryRepository

這裡寫圖片描述
ProductTotalizer

接下來,我們使用 ASP.NET Core 自帶的 DI 來註冊服務:

這裡寫圖片描述
註冊服務

可以看到,註冊物件有很多種方法,並且我們可以管理物件的生命週期。註冊完物件,我們就可以在我們需要的地方注入對應的物件了,還是最簡單的例子——在控制器中使用:

這裡寫圖片描述
注入到控制器中

對於控制器,我們有三種方式注入物件:建構函式、控制器動作、屬性注入。然而,在一般的類中,使用自帶的 DI 只能是建構函式注入。到底是哪種方式好,見仁見智。

###3.2生命週期

ASP.NET Core 服務可以被配置為以下生命週期:

  • 瞬時(Transient) 在它們每次請求時都會被建立。這一生命週期適合輕量級的,無狀態的服務。
  • 作用域 (Scoped) 在每次請求中只建立一次。
  • 單例(Singleton) 在它們第一次被請求時建立(或者如果你在
    ConfigureServices執行時指定一個例項)並且每個後續請求將使用相同的例項。

我們將通過逐步更改 IRepository 的生命週期來看看會發生什麼事情。
首先是瞬時:

這裡寫圖片描述
瞬時

接著是作用域:

這裡寫圖片描述
作用域

最後是單例:

這裡寫圖片描述
單例

瞬時很好理解,類似每次都會new了一個物件。而對於作用域,如果一次請求中,有兩個甚至多個非單例物件引用到同一個作用域型別時,他們將會收穫同一個例項。單例也很好理解,從頭到尾都是同一個例項。

控制單一變數,如果只是改變 ProductTotalizer 的生命週期而不是改變 IRepository 的生命週期的話,會發生什麼情況呢?

###3.3依賴注入小貼士

  • 遵循 SOLID 原則,考慮一下哪些是需要依賴注入的,避免硬編碼。
  • 合理考慮生命週期,假如某個型別在不同上下文中需要不同生命週期時,是否需要顯式命名區分?還是考慮結構是否合理?
  • 避免靜態訪問服務。
  • 避免靜態訪問 HttpContext

##4.啟用擴充套件

在專案中我們往往會新增許多擴充套件,比如用於API文件說明的Swagger、計劃任務的Hangfire、壓縮響應的GZIP、跨域訪問、日誌擴充套件等等。他們的共同點就是需要先安裝相應的nuget包,然後在 ConfigureServices() 方法中配置服務,最後在 Configure() 方法中啟用。
我們以Swagger為例,首先是安裝對應的 nuget 包—— Swashbuckle
接著是配置擴充套件:

這裡寫圖片描述
配置Swagger

最後就是啟用 Swagger 了:

這裡寫圖片描述
啟用 Swagger

最後我們訪問 Swagger 的地址看看效果:

這裡寫圖片描述
線上API文件

##5.中介軟體

中介軟體是用於組成應用程式管道來處理請求和響應的元件。管道內的每一個元件都可以選擇是否將請求交給下一個元件、並在管道中呼叫下一個元件之前和之後執行某些操作。請求委託被用來建立請求管道,請求委託處理每一個 HTTP 請求。

這裡寫圖片描述
中介軟體處理請求

舉一個簡單的例子(更復雜的可以在中介軟體依賴注入物件),從 cookie 中獲取 token 並附加到請求頭中:

這裡寫圖片描述
獲取 cookie 中的token

與啟用擴充套件一樣,我們同樣是需要在 Configure()方法中啟用中介軟體:

 app.UseMiddleware<JWTInHeaderMiddleware>();

如果我們有多箇中介軟體呢,中介軟體的順序可能會影響到響應結果,但並不是總是線性相關的。例如,我們新增一個對響應狀態碼處理的中介軟體:
響應狀態碼處理中介軟體

我們把它加到其他中介軟體的最前面:

app.UseMiddleware<StatusCodeMiddleware>();
//....
app.UseMiddleware<JWTInHeaderMiddleware>();

雖然對狀態碼處理的中介軟體是最前面,但可以在請求的最後關頭對請求結果進行處理。當然,如果中間有某個中介軟體短路了(沒有傳遞到下一個中介軟體),就會讓我們前功盡棄。

這裡寫圖片描述
測試多箇中介軟體處理請求

##6.過濾器

與中間相似,過濾器同樣可以對請求的前後執行特定程式碼,但是過濾器可以配置為全域性有效、僅對控制器有效或是僅對 Action 有效,比中介軟體更具有靈活性。

這裡寫圖片描述
過濾器處理請求

另外,過濾器從型別上還能分為:授權過濾器、資源過濾器、Action過濾器、結果過濾器。很容易實現面向切面程式設計,降低了耦合。
這裡舉一個我最喜歡的過濾器——對請求的模型進行驗證:

這裡寫圖片描述
請求模型過濾器

在控制器或 Action 使用,只需加上特性即可:

這裡寫圖片描述
控制器中使用過濾器

當然,模型驗證的過濾器往往具有全域性性,所以我一般是加在 services.AddMvc 中:

services.AddMvc(config=> 
{
     config.Filters.Add(new ValidateModel());
 });

這裡寫圖片描述
模型驗證過濾器效果

##7.工作環境

ASP.NET Core 提供了許多功能和約定來允許開發者更容易的控制在不同的環境中他們的應用程式的行為。當釋出一個應用程式從開發到預演再到生產,為環境設定適當的環境變數允許對應用程式的除錯,測試或生產使用進行適當的優化。

在軟體開發的生命週期中,在不同的工作環境中往往是不同的狀態。比如說,在測試或者預演環境中,啟用Swagger擴充套件、控制檯列印日誌、允許跨域,而在生產環境中,往往處於安全、效能或者其他考慮,這些功能是需要禁止的。對於 MVC 開發者來說,在開發過程中會使用本地的JS、Css、圖片等檔案,而在生產環境中這些檔案往往是從CDN中獲取。

這裡寫圖片描述
配置工作環境

##8.結語

ASP.NET Core 提供了強大而靈活的配置機制,每個點展開都像是一片新的天地。即使是經驗豐富的開發者,也不能自稱完全掌握全部機制。隨著 .NET Core 的完善和發展,Startup.cs 也將越來越複雜。越是複雜,就越是要小心,如無需要,勿增實體!

相關文章