上一部分預備知識在這 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里面的內容, 還有系統環境變量的值.
這種情況下, 使用IServiceCollection的Configure擴展方法可以把配置映射到指定的類上面:
同時這也允許在程序的任何地方注入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的正題了.