用ASP.NET Core 2.0 創建規範的 REST API -- 預備知識 (2) + 準備項目

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

若是您對ASP.NET Core很瞭解的話,能夠不看本文, 本文基本都是官方文檔的內容。數據庫

ASP.NET Core 預備知識

項目配置

假設在項目的根目錄有這樣一個json文件, 在ASP.NET Core項目裏咱們可使用IConfigurationRoot來使用該json文件做爲配置文件, 而IConfigurationRoot是使用ConfigurationBuilder來建立的:json

能夠看到ConfigurationBuilder加載了firstConfig.json文件, 使用的是AddJsonFile這個擴展方法. 調用builder的Build方法會獲得一個IConfigurationRoot的實例, 它實現了IConfiguration接口, 隨後咱們即可以經過遍歷它的鍵值對.windows

其中json文件裏的結構數據都最爲鍵值對被扁平化到IConfiguration裏了, 咱們能夠經過它的key找到對應的值:服務器

像childkey1這種帶層次結構的值可使用冒號 做爲層次分隔符.mvc

配置文件總會包含這種多層結構的, 更好的辦法是把相似的配置進行分組獲取, 可使用IConfiguration的GetSection()方法來獲取局部的配置:app

 

當有多個配置文件的時候, 配置數據的加載和它們在程序中指定的順序是同樣的, 若是多個文件都有同一個鍵的話, 那麼最後加載的值將會覆蓋先前加載的值.less

下面是另外一個配置文件:異步

在firstConfig後加載secondConfig:post

最後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的正題了.

相關文章
相關標籤/搜索