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

1.前言

本文主要是以Visual Studio 2017 默認的 WebApi 模板作爲基架,基於Asp .Net Core 1.0,本文面向的是初學者,如果你有 ASP.NET Core 相關實踐經驗,歡迎在評論區補充。
與早期版本的 ASP.NET 對比,最顯著的變化之一就是配置應用程序的方式, Global.asax、FilterConfig.cs 和 RouteConfig.cs 統統消失了,取而代之的是 Program.cs 和 Startup.cs。Program.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);

最後就是解決怎麼使用這些配置參數的問題了,舉個最簡單的例子,我們可以在某個控制器中把我們的所有參數打印出來:


 
 

 


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

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

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

 

我們可以發現返回的對象裏面的屬性都爲null,這是因爲json中的 "wizards"的節點並不能與我們的類匹配。
那麼問題來了,如果匹配的代碼如下,又會產生什麼樣的結果呢?先別急着回答,我會在下一節中給出答案。

2.2環境變量

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


 

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


 

在這之前,我們的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 的地址看看效果:


 

5.中間件

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


中間件處理請求


舉一個簡單的例子(更復雜的可以在中間件依賴注入對象),從 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 也將越來越複雜。越是複雜,就越是要小心,如無需要,勿增實體!

9.相關引用

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章