用ASP.NET Core 2.0 建立規範的 REST API -- 預備知識 (2) + 準備專案

solenovex發表於2018-05-12

上一部分預備知識在這 http://www.cnblogs.com/cgzl/p/9010978.html

如果您對ASP.NET Core很瞭解的話,可以不看本文, 本文基本都是官方文件的內容。

ASP.NET Core 預備知識

專案配置

假設在專案的根目錄有這樣一個json檔案, 在ASP.NET Core專案裡我們可以使用IConfigurationRoot來使用該json檔案作為配置檔案, 而IConfigurationRoot是使用ConfigurationBuilder來建立的:

可以看到ConfigurationBuilder載入了firstConfig.json檔案, 使用的是AddJsonFile這個擴充套件方法. 呼叫builder的Build方法會得到一個IConfigurationRoot的例項, 它實現了IConfiguration介面, 隨後我們便可以通過遍歷它的鍵值對.

其中json檔案裡的結構資料都最為鍵值對被扁平化到IConfiguration裡了, 我們可以通過它的key找到對應的值:

像childkey1這種帶層次結構的值可以使用冒號 作為層次分隔符.

配置檔案總會包含這種多層結構的, 更好的辦法是把類似的配置進行分組獲取, 可以使用IConfiguration的GetSection()方法來獲取區域性的配置:

 

當有多個配置檔案的時候, 配置資料的載入和它們在程式中指定的順序是一樣的, 如果多個檔案都有同一個鍵的話, 那麼最後載入的值將會覆蓋先前載入的值.

下面是另一個配置檔案:

在firstConfig後載入secondConfig:

最後key1的值是後載入的secondConfig裡面的值.

當然了, 如果firstConfig裡面有而secondConfig卻沒有的鍵, 它的值肯定來自firstConfig.

 

配置提供商

配置資料可以來自多種資料來源, 它們可能是不同格式的.

ASP.NET Core 預設支援從下列方式獲得配置:

  • 檔案格式(INI, JSON, XML)
  • 命令列引數
  • 環境變數
  • 記憶體中的.NET物件
  • 未加密的Secret管理儲存
  • 加密的使用者儲存, 例如Azure祕鑰庫
  • 自定義的提供商

這些東西還是看官方文件吧, 本文使用JSON格式的就夠用了.

 

強型別的配置

ASP.NET Core允許把配置資料對映到一個物件類上面.

針對上面的firstConfig.json檔案, 我們建立以下這個類:

然後呼叫IConfiguration的Bind擴充套件方法來把鍵值對集合對值對映到這個強型別對POCO例項裡:

 

在標準的ASP.NET Core 2.0的專案模版裡, 載入配置檔案的步驟被封裝了, 預設或載入appSettings.json 以及 appSettings.{環境}.json.

我記得是封裝在這裡了:

我把firstConfig.json改名為appSettings.json.

然後在Startup裡面可以獲得IConfiguration:

從列印結果可以看到, 載入的不只是appSettings裡面的內容, 還有系統環境變數的值.

這種情況下, 使用IServiceCollectionConfigure擴充套件方法可以把配置對映到指定的類上面:

同時這也允許在程式的任何地方注入IOptions<FirstConfig>了:

這個Configure方法不僅僅可以對映ConfigurationRoot, 還可以對映配置的一部分:

 

配置變化

在專案執行的時候, 專案的配置資訊可能會發生變化.

當採用的是基於檔案的配置時, 如果配置資料有變化了, 我們應該讓配置模型重新載入, 這就需要把AddJsonFile裡面的配置屬性 ReloadOnChange 設定為 true:

這時, 無論在哪各地方使用了IConfigurationRoot和IConfiguration, 它們都會反映出最新的值, 但是IOptions<T>卻不行. 即使檔案變化了並且配置模型也通過檔案提供商進行了更新, IOptions<T>的例項仍然包含的是原始值.

為了讓配置資料可以在這種強型別對映的類上體現, 就需要使用IOptionsSnapshot<T>:


IOptionsSnapshot<T> 的開銷很小, 可以放心使用

 

日誌 

ASP.NET Core 提供了6個內建的日誌提供商。

需要使用日誌的話,只需注入一個ILogger物件即可,不過該物件首先要在DI容器中註冊。

這個ILogger介面主要是提供了Log方法:

記錄Log的時候使用Log方法即可:

不過可以看到,該方法引數很多,用起來還是略顯麻煩的。

幸運的是,針對Log還有幾個擴充套件方法,他們就簡單了很多:

  • LogCritical,用來記錄嚴重的事情
  • LogDebug,記錄除錯資訊
  • LogError,記錄異常
  • LogInformation,記錄資訊性的事情
  • LogTrace,記錄追蹤資訊
  • LogWarning,記錄警告資訊

 

在專案中配置和使用Log,只需在Program.cs裡呼叫IWebHostBuilder的ConfigureLogging擴充套件方法即可:

本例中,我們把log配置成在控制檯輸出。

如果只是輸出到控制檯,其實我們就多此一舉了,因為CreateDefaultBuilder這個方法裡已經做了一些Log的配置,看一下反編譯的原始碼:

可以看到logging的一些配置資料是從整體配置的Logging部分取出來的,然後配置了使用輸出到控制檯和Debug視窗的提供商。

記錄Log的時候,通常情況下使用那幾個擴充套件方法就足夠了:

請注意,這裡我注入的是ILogger<T>型別的logger,其中T可以用來表示日誌的分類,它可以是任何型別,但通常是記錄日誌時所在的類。

執行專案後,可以看到我記錄的日誌:

 

同樣也可以在一個類裡面把記錄的日誌分為不同的分類,這時候你可以使用ILoggerFactory,這樣就可以隨時建立logger了,並把它繫結到特定的區域:

不知道您有沒有發現上面這幾個例子中日誌輸出的時候都有個數字 [0], 它是事件的識別符號。因為上面的例子中我們沒有指定事件的ID,所以就取預設值0。使用事件ID還是可以幫助我們區分和關聯記錄的日誌的。

 

每次寫日誌的時候, 都需要通過不同的方式指明LogLevel, LogLevel表明的是嚴重性.

下面是ASP.NET Core裡面定義的LogLevel(它是個列舉), 按嚴重性從低到高排序的:

Trace = 0, 它可以包含敏感拘束, 預設在生產環境中它是被禁用掉的.

Debug = 1, 也是在除錯使用, 應該在生產環境中禁用, 但是遇到問題需要除錯可以臨時啟用.

Information = 2, 用來追蹤應用程式的總體流程.

Warning = 3, 通常用於記錄非正常或意外的事件, 也可以包括不會導致應用程式停止的錯誤和其他事件, 例如驗證錯誤等.

Error = 4, 用於記錄無法處理的錯誤和異常, 這些資訊意味著當前的活動或操作發生了錯誤, 但不是應用程式級別的錯誤.

Critical = 5, 用於記錄需要立即處理的事件, 例如資料丟失或磁碟空間不足.

None = 6, 如果你不想輸出日誌, 你可以把程式的最低日誌級別設定為None, 此外還可以用來過濾日誌.

 

記錄的日誌資訊是可以帶引數的, 使用訊息模板(也就是訊息主題和引數分開), 格式如下:

同樣也支援字串插值:

第二種方式程式碼的可讀性更強一些, 而且它們輸出的結果沒有什麼區別:

但是對於日誌系統來說, 這兩種方式是不一樣的. 通過訊息模板的方式(訊息和引數分開的方式), 日誌提供商可以實現語義日誌或叫做結構化日誌, 它們可以把引數單獨的出入到日誌系統裡面進行單獨儲存, 不僅僅是格式化的日誌資訊.

此外, 用過載的方法, 記錄日誌時也可以包含異常物件.

 

日誌分組

我們可以使用相同的日誌資訊來表示一組操作, 這需要使用scope, scope繼承了IDisposable介面, 通過ILogger.BeginScope<TState>可以得到scope:

使用scope, 還有一點需要注意, 需要在日誌提供商上把IncludeScopes屬性設定為true:

您可以發現, 日誌被輸出了兩遍, 這是因為WebHost.CreateDefaultBuilder方法裡面已經配置使用了AddConsole()方法, 我再配置一遍的話就相當於又新增了一個輸出到控制檯的日誌提供商.

所以, 我可以不採用這個構建模式建立IWebHost, 改為直接new一個:

這樣就正確了. 可以看到日誌資訊的第一行內容是一樣的, 第二行是各自的日誌資訊.

 

日誌的過濾

我們可以為整個程式設定日誌記錄的最低階別, 也可以為某個日誌提供商和分類指定特定的過濾器.

設定全域性最低記錄日誌的級別使用SetMinimumLevel()擴充套件方法:

如果想完全不輸出日誌的話, 可以把最低記錄的級別設為LogLevel.None.

我們還可以為不同場景設定不同的最低記錄級別:

然後分別建立這兩個分類的logger, 並記錄:

檢視輸出結果, 已經按配置進行了過濾:

這裡可以使用完整的類名作為分類名:

然後使用ILogger<T>即可:

 

針對上面這個例子, 我們還可以使用配置檔案:

相應的, 程式碼也需要改一下:

輸出的效果是一樣的.

 

日誌提供商

ASP.NET Core 內建了6個日誌提供商:

  • Console, 使用logging.AddConsole()來啟用.
  • Debug, 使用logging.AddDebug()來啟用. 它使用的是System.Diagnostics.Debug的Debug.WriteLine()方法, 由於Debug類的所有成員都是被[Conditional("DEBUG")]修飾過了, 所以無法被構建到Release Build裡, 也就是生產環境是無法輸出的, 除非你把Debug Build作為部署到生產環境?.
  • EventSource, 使用logging.AddEventSourceLogger()來啟用. 它可以把日誌記錄到事件追蹤器, 它是跨平臺的, 在windows上, 會記錄到Event Tracing for Windows (ETW)
  • EventLog (僅限Windows), 使用logging.AddEventLog()來啟用. 它會記錄到Windows Event Log.
  • TraceSource (僅限Windows),, 使用logging.AddTraceSource(sourceSwitchName)來啟用. 它允許我們把日誌記錄到各種的追蹤監聽器上, 例如 TextWriterTraceListener
  • Azure App Service, 在本地執行程式的時候, 這個提供商並不會起作用, 部署到Azure App Service的.NET Core程式會自動採用該提供商, .NET Core無須呼叫logging.AddAzureWebAppDiagnostics();該方法. 它會把日誌記錄到Azure App Service app的檔案系統還會寫進Azure Storage賬戶的blob storage裡. 

第三方日誌提供商

第三方的提供商有很多: Serilog, NLog, Elmah.IO, Loggr, JSNLog等等.

 

處理異常

ASP.NET Core 未開發人員提供了一個異常資訊頁面, 它是執行時生成的, 它封裝了異常的各種資訊, 例如Stack trace.

 

可以看到只有執行環境是開發時才啟用該頁面, 上面我丟擲了一個異常, 看看訪問時會出現什麼結果:

 

這就是異常頁面, 裡面包含異常相關的資訊.

注意: 該頁面之應該在開發時啟用, 因為你不想把這些敏感資訊在生產環境中暴露.

 

當傳送一個請求後, HTTP機制提供的響應總是帶著一個狀態碼, 這些狀態碼主要有:

  • 1xx, 用於通知報告.
  • 2xx, 表示響應是成功的, 例如 200 OK, 201 Created, 204 No Content.
  • 3xx, 表示某種重定向, 
  • 4xx, 表示客戶端引起的錯誤, 例如 400 Bad Request, 401 Unauthorized, 404 Not Found
  • 5xx, 表示伺服器錯誤, 例如 500 Internal Server Error.

 

預設情況下, ASP.NET Core 專案不提供狀態碼的細節資訊, 但是通過啟用StatusCodePagesMiddleware中介軟體, 我們可以啟用狀態碼細節資訊:

然後當我們訪問一個不存在的路由時, 就會返回以下資訊:

我們也可以自定義返回的狀態碼資訊:

 

OK, 預備知識先介紹到這, 其它相關的知識在建立API的時候穿插著講吧.

專案開始模板

非常的簡單, 先看一下Program.cs:

我們使用了WebHost.CreateDefaultBuilder()方法, 這個方法的預設配置大約如下:

採用Kestrel伺服器, 使用專案個目錄作為內容根目錄, 預設首先載入appSettings.json, 然後載入appSettings.{環境}.json. 還載入了一些其它的東西例如環境變數, UserSecrect, 命令列引數. 然後配置Log, 會讀取配置資料的Logging部分的資料, 使用控制檯Log提供商和Debug視窗Log提供商, 最後設定了預設的服務提供商.

然後我新增了自己的一些配置:

使用IIS作為反向代理伺服器, 使用Url地址為http://localhost:5000, 使用Startup作為啟動類.

然後看Startup:

主要是註冊mvc並使用mvc.

隨後建立Controllers資料夾, 然後可以新增一個Controller試試是否好用:

 

可選專案配置

注意, 在使用VS2017啟動專案的時候, 上面有很多選項:

為了開發時方便, 我把IISExpress這個去掉, 開啟並編輯這個檔案:

刪掉IISExpress的部分, 然後修改一下applicationUrl:

然後啟動選項就只剩下一個了:

 

如果你喜歡使用dotnet cli, 可以為專案新增dotnet watch, 開啟並編輯 MyRestful.Api.csproj, 新增這行即可:

然後命令列執行 dotnet watch run 即可, 每次程式檔案發生變化, 它都會重新編譯執行程式:

 

為專案新增EntityFrameworkCore 2.0

關於EFCore 2.0的知識, 還是請看官方文件吧, 我也寫了一篇非常非常入門級的文章, 僅供參考: http://www.cnblogs.com/cgzl/p/8543772.html

新建立兩個.NET Core class library型別的專案:

這幾個專案的關係是: MyRestful.Infrastructure 需要引用 MyRestful.Core, MyRestful.Api 需要引用其他兩個.

 

 並把它們新增到MyRestful.Api專案的引用裡.

然後要為MyRestful.Infrastructure專案新增幾個包, 可以通過Nuget或者Package Manager Console或者dotnet cli:

Microsoft.EntityFrameworkCore.SqlServer (我打算使用記憶體資料庫, 所以沒安裝這個)

Microsoft.EntityFrameworkCore.Tools

 

然後在MyRestful.Infrastructure專案裡面建立一個DbContext:

 

再建立一個Domain Model, 因為Model和專案的合約(介面)一樣都是專案的核心內容, 所以把Model放在MyRestful.Core專案下:

 

然後把這個Model放到MyContext裡面:

在Startup.cs裡面註冊DbContext, 我使用的是記憶體資料庫:

這裡要注意: 由於使用的是記憶體資料庫, 所以遷移等一些配置都可以省略了....

做一些種子資料:

這時需要修改一下Program.cs 來新增種子資料:

 好的, 到現在我寫一些臨時的程式碼測試一下MyContext:

直接從資料庫中讀取Domain Model 然後返回, 看看效果(這次使用的是POSTMAN):

可以看到, MyContext是OK的.

到這裡, 就會出現一個問題, Controller的Action方法(也就是Web API吧)應該直接返回Domain Model嗎?

你也可能知道答案, 不應該這樣做. 因為:

像上面例子中的Country這樣的Domain Model對於整個程式來說是內部實現細節, 我們肯定是不想把內部實現細節暴露給外部的, 因為程式是會變化的, 這樣就會對所有依賴於這個內部實現的客戶端造成破壞. 所以我們需要在內部實現外面再加上另外一層, 這層裡面的類就會作為整個程式的公共合約或公共介面(介面的意思, 不是指C#介面).

可以把這件事想象比喻成組裝電腦:

組裝電腦機箱裡有很多零件: 主機板, 硬碟, CPU, 記憶體.....這就就是內部實現細節, 而使用者能看到和用到的是前後皮膚的介面和按鈕, 這就是我所說的電腦機箱的公共合約或公共介面. 更重要的是, 組裝電腦的零件可能會更新換代, 也許新增一條記憶體, 換個固態硬碟.....但是所有的這些變化都不會改變(基本上)機箱前後皮膚的介面和按鈕. 這個概念對於軟體程式來說是一樣的, 我們不想暴露我們的Domain Model給客戶端, 所以我們需要另外一套Model類, 它們要看起來很像我們的Domain Model, 但是這兩種model可以獨立的進化和改變.

這類Model會到達程式的邊界, 作為Controller的輸入, 然後Controller把它們序列化之後再輸出. 

用REST的術語來說, 我們把客戶端請求伺服器返回的物件叫做資源(Resources).

所以我會在MyRestful.Api專案裡建立一個Resources資料夾, 並建立一個類叫做CountryResource.cs (以前我把它叫ViewModel或Dto, 在這裡我叫它Resource, 都是一個意思):

現在來說, 它的屬性和Country是一樣的.

 

現在的問題是我要把MyContext查詢出來的Country對映成CountryResource, 你可以手動編寫對映關係, 但是最好的辦法還是使用AutoMapper庫(有兩個), 安裝到MyRestful.Api專案:

AutoMapper AutoMapper.Extensions.Microsoft.DependencyInjection

然後我們要做兩個對映配置檔案, 分別是Domain Model ==> Resource 和 Resource ==> Domain Model:

當然了, 也可以做一個配置檔案, 我還是做一個吧:

然後在Startup裡面註冊AutoMapper即可:

 

 修改Controller測試下:

結果是OK的:

 

Repository 模式

概念不說了, 你可以把Repository想象成就是一堆Domain Models, 我們可以使用這個模式來封裝查詢等操作. 例如下面紅框裡面的查詢:

這個查詢有可能在整個專案中的多個地方被使用, 在稍微大一點的專案裡可能會有很多類似的查詢, 而Repository模式就是可以解決這個問題的一種方式. 

所以我在MyRestful.Infrastructure專案裡建立Repostitories資料夾並建立CountryRepostsitory類:

這裡需要注入MyContext, 暫時只需要一個查詢方法.

現在Repository做好了, 為了在Controller裡面使用(依賴注入), 我們需要為它抽取出一個介面, 因為我們不想讓Controller與這些實現緊密的耦合在一起, 我們需要做的是把Controller和介面給耦合到一起, 這也就是依賴反轉原則(DIP, 也就是SOLID裡面的D, 高階別的模組不應該依賴於低階別的模組, 它們都應該依賴於抽象):

此外, 單元測試的時候, 我們可以用實現了IRepository的假Repository, 因為單元測試的時候最好不要依賴外界的資源, 例如資料庫, 檔案系統等, 最好只用記憶體中的資料.

所以先抽取介面:

然後配置DI:

在這裡ASP.NET Core 提供了三種模式註冊實現給介面, 它們代表著不同的生命週期:

  • Transient: 每次請求(不是指HTTP Request)都會建立一個新的例項,它比較適合輕量級的無狀態的(Stateless)的service。
  • Scope: 每次http請求會建立一個例項。
  • Singleton: 在第一次請求的時候就會建立一個例項,以後也只有這一個例項,或者在ConfigureServices這段程式碼執行的時候建立唯一一個例項。

由於Repository依賴於DbContext, 而DbContext在ASP.NET Core專案配置裡是Scope的, 所以每次HTTP請求的生命週期中只有一個DbContext例項, 所以IRepository就應該是Scope的.

修改Controller, 注入並使用IRepository, 去掉MyContext:

經測試, 結果是一樣的, 我就不貼圖了.

 

還有一個問題, 因為每次HTTP請求只會存在一個MyContext的例項, 而引用該例項的Repository可能是多個. 也就是說會存在這種情況, 某個Controller的Action方法裡, 使用了多個不同的Repository, 分別做了個新增, 修改, 刪除等操作, 但是儲存的時候還是需要MyContext來做, 把儲存動作放到任何一個Repository裡面都是不合理的. 而且我之前講過應該把Repository看作是Domain Models的集合, 例如list, 而list.Save()也沒有什麼意義. 所以Controller還是依賴於MyContext, 因為需要它的Save動作, 還是需要解耦. 

之前講的使用Repository和依賴注入解耦的方式很大程度上較少了重複的程式碼, 而把Controller和EFCore解耦還有另外一個好處, 因為我有可能會把EFCore換掉, 去使用Dapper ?, 因為如果專案比較大, 或者越來越大, 有一部分業務可能會需要效能比較好的Micro ORM來代替或者其它儲存方式等. 所以引用EFCore的地方越少, 就越容易替換.

這時, 就應該使用Unit Of Work 模式了, 首先我新增一個IUnitOfWork的介面, 我把它放在MyRestful.Core專案的interfaces資料夾下了:

只有一個非同步方法SaveAsync(). 然後是它的實現類UnitOfWork:

就是這樣, 如果你想要替換掉Entity Framework Core的話, 只需要修改UnitOfWork和Repository, 無須修改IUnitOfWork和IRepository, 因為這些介面是專案的合約, 可以看作是不變的 (所以IRepository也應該放在MyRestful.Core裡面, 這個以後再改).

然後註冊DI:

修改Controller注入IUnitOfWork試試:

這裡我又給Repository新增了一個Add方法用於測試, 結果如下:

好的, 沒問題.

 

整體結構調整

差不多了, 讓我們再回顧以下DIP原則(依賴反轉): 高階別模組不應該依賴於低階別模組, 它們都應該依賴於抽象. 如果把Repository看作是服務的話, 那麼使用服務的模組(Controller)就是高階別模組, 服務(Repository)就是低階別模組. 這個問題我們已經解決了. 

為什麼要遵循這個原則? 因為要減少程式變化帶來的影響.

看這張圖:

就從一個方面來說, 如果Repository變化或重編譯了, 那麼Controller很有可能會變化並肯定需要重新編譯, 也就是所有依賴於Repository的類都會被重新編譯.

而使用DIP原則之後:

我們可以在Repository裡面做出很多更改, 但是這些變化都不會影響到Controller, 因為Controller並不是依賴於這個實現.

只要IRepository這個介面不發生變化, Controller就不會被影響到. 這也就可能會較少對整個專案的影響.

 

Interface 代表的是 "是什麼樣的", 而實現代表的是 "如何去實現".

Interface一旦完成後是很少改變的.

針對使用Repository+UnitOfWork模式的專案結構, 有時會有一點錯誤的理解, 可能會把專案的結構這樣劃分:

這樣一來, 從名稱空間角度講. 其實就是這樣的:

高階別的包/模組依賴於低階別的包/模組.

也就違反了DIP原則, 所以如果想按原則執行, 就需要引進一個新的模組:

把所有的抽象相關的類都放在Core裡面.

這樣就滿足了DIP原則.

所以我們把專案稍微重構以下, 把合約/介面以及專案的核心都放在MyRestful.Core專案裡:

 

好的, 這次先寫道這裡, 專案已經做好了最基本的準備, 其餘功能的擴充套件會隨著後續文章進行.

下面應該快要切入REST的正題了.

相關文章